Introduction

Since QGis 2.0, you can use GeoAlgorithms (aka Processing or geoprocessing) to manipulate your data. But you can also developp your own GeoAlgo to do things that are not (already) included in QGis. As GeoAlgo is relatively young, the development on this part of QGis is important.

Today I propose a very basic GeoAlgo that takes an Oracle layer, use internal Oracle Spatial procedure (SDO_GEOM.VALIDATE_GEOMETRY_WITH_CONTEXT) to find non valid geometries and returns a layer with those geometries and an explanation of the errors that have been found.

Install cx_Oracle

The code needs cx_Oracle Python library to work. This library is a low-level communication Python (DBAPIv2) library for interconnections to Oracle databases. For the moment it is not included in QGis binary installation, so you have to install by yourself first.

If you are running a GNU/Linux distribution and have installed QGis from the official repositories, you'll have to install Oracle Client and SDK and then install cx_Oracle. Read this...

If you are running QGis on MS-Windows, you have to download the good version of cx_Oracle binaries (32 or 64 bits) and extract and copy cx_Oracle.pyd file in C:\Program Files\QGis Chugiak/apps/Python2.7/DLL .

To check for a valid installation of cx_Oracle in the QGis Python environment:

  • Launch QGis
  • Open the Python Console (Plugins -> Python Console)
  • Type:
import cx_Oracle
  • If you have no error message, you're done !

Source code for VerifyOracle.py

Here is the source of the GeoAlgo. Just save it in a file named VerifyOracle.py ans place it in the good directory. You'll have to find the directory that stores the official Processing plugin.

On MS-Windows systems, it is stored in C:\Program Files\QGIS Chugiak\apps\qgis\python\plugins\processing\algs\qgis.

# -*- coding: utf-8 -*-


#***************************************************************************
#    VerifyOracle.py
#    ---------------------
#    Date                 : September 2014
#    Copyright            : (C) 2014 by Médéric RIBREUX
#    Email                : mederic.ribreux@medspx.fr
#***************************************************************************
#*                                                                         *
#*   This program is free software; you can redistribute it and/or modify  *
#*   it under the terms of the GNU General Public License as published by  *
#*   the Free Software Foundation; either version 2 of the License, or     *
#*   (at your option) any later version.                                   *
#*                                                                         *
#***************************************************************************
#
#You need to install cx_Oracle under QGis Python directory.

__author__ = 'Médéric RIBREUX'
__date__ = 'September 2014'
__copyright__ = '(C) 2014, Médéric RIBREUX'

# This will get replaced with a git SHA1 when you do a git archive

__revision__ = '$Format:%H$'

from osgeo import gdal
import cx_Oracle
import re
from qgis.core import *
from PyQt4.QtCore import *
from processing.core.GeoAlgorithm import GeoAlgorithm
from processing.parameters.ParameterVector import ParameterVector
from processing.parameters.ParameterTableField import ParameterTableField
from processing.outputs.OutputVector import OutputVector
from processing.tools import dataobjects, vector
from processing.tools.general import *


class VerifyOracle(GeoAlgorithm):

    INPUT_VECTOR = 'INPUT_VECTOR'
    OUTPUT = 'OUTPUT'
    FIELD = 'FIELD'

    def defineCharacteristics(self):
        self.name = 'Verify layer geometries with Oracle Spatial engine'
        self.group = 'Vector analysis tools'

        self.addParameter(ParameterVector(self.INPUT_VECTOR, 'Vector layer',
                          [ParameterVector.VECTOR_TYPE_ANY]))
        self.addParameter(ParameterTableField(self.FIELD, 'UID Field for input vector layer',
                                              self.INPUT_VECTOR))
        self.addOutput(OutputVector(self.OUTPUT, 'Result Vector layer'))

    def processAlgorithm(self, progress):

        uri = self.getParameterValue(self.INPUT_VECTOR)
        layer = dataobjects.getObjectFromUri(uri)
        # Deals with fields
        fieldName = self.getParameterValue(self.FIELD)
        fieldIdx = layer.fieldNameIndex(fieldName)
        fields = layer.dataProvider().fields()

        # Add the Errors field
        fields.append(QgsField('Errors', QVariant.String))

        # Get connection parameters
        regexp = re.compile(".*dbname='(+)'.*user='(+).*password='(+)'.*table=(+).*\((+)\)")
        dbname, user, password, table, geocol = regexp.match(uri).groups()

        query = u"SELECT c.{0}, SDO_GEOM.VALIDATE_GEOMETRY_WITH_CONTEXT (c.{1}, 0.001) FROM {2} c WHERE SDO_GEOM.VALIDATE_GEOMETRY_WITH_CONTEXT (c.{1}, 0.001) <> 'TRUE'".format(self.getParameterValue(self.FIELD),geocol, table)

        # Make the connection and the query
        connection = cx_Oracle.connect( user, password, dbname)
        c = connection.cursor()
        c.execute(query)
    rows = c.fetchall()
        c.close()

        # Open a writer to the output vector layer
        writer = self.getOutputFromName(
                self.OUTPUT).getVectorWriter(fields,
                                             layer.dataProvider().geometryType(),
                                             layer.dataProvider().crs())


        # We get all of the features from the input vector layer
        features = vector.features(layer)

        # And make some computations for the progress bar
        total = 100.0 / float(len(features))
        current = 0

        outFeat = QgsFeature()


        # Build a list of errors (at least as big as number of features of the layer)
        errors = []
        for row in rows:
            errors.append({'GID':row[0], 'ERROR':row[1]})

        # Main loop
        for feature in features:
            gid = feature.attributes()[fieldIdx]
            # if the feature has got an error
            if gid in [x['GID'] for x in errors]:
                error = (x['ERROR'] for x in errors if x['GID'] == gid).next()
                geom = feature.geometry()
                attrs = feature.attributes()

                # write the feature to the output layer
                outFeat.setGeometry(geom)
                attrs.append(error)
                outFeat.setAttributes(attrs)
                writer.addFeature(outFeat)

            current += 1
            progress.setPercentage(int(current * total))

        del writer

Register GeoAlgo

For QGis to display the GeoAlgo in the algorithms tree, you have to register the Python module first. Registering the module is done by editing the file named QGISAlgorithmProvider.py. Just add the following lines:

...
# on the declaration of modules
from VerifyOracle import VerifyOracle
...
        self.alglist = [SumLines(), PointsInPolygon(), # just add a line to this list
...
                        VerifyOracle(),

In action

Just open an Oracle layer in QGis, then launch the GeoAlgo. It is located in QGis GeoAlgorithms and in Vector analysis tools:

GeoAlgo menu

Then you'll find the following dialog box:

GeoAlgo dialog box

  • In Vector layer choose the layer you want to verify.
  • In UID Field for vector layer choose an attribute which is a primary key for the layer.
  • In Result vector layer, you can add a path to a new shapefile or leave it blank to use a temporary file.
  • Click on the Run button.

Once run and done, you can find a layer named "Result Vector Layer" that contains the non-valid geometries. Every attributes are taken from the original layer and one column has been added at the end, which is named Errorsand contains what errors have been found by Oracle. The ring and edges numbers are very useful to find where is the problem as QGis uses the same numbers in its nodes tool:

GeoAlgo results

Conclusion

This code is provided as is ! There is plenty of room for improvement. You should consider it as something experimental... But it works !

Posted mer. 01 oct. 2014 18:34:01

Introduction

Since QGis 1.8 (and even before), you can use QGis to build forms to deal with the attributes when you create a new geographic object. Over the successive version of QGis, form building get plent of new features like a 1,1 value relation (v2.2) and now 1,n relations (v2.2). You can also use the autobuild feature with which QGis builds the form depending on what you put in the fields properties. There is alos a way to build you own form control with QTCreator ui files. Of course, you can control the form logic with Python code.

In the development of a specific form I had to build a cascading control: the value of one control is linked to the value of an upper control (controls are linked together). This is especially convenient for narrowing a choice. For example you want to choose a district or an area that restricts a list of counties that itself restricts a list of towns. I call this cascading form controls.

For the moment, QGis don't have an internal mechanism to deal with those controls. I've sent a feature request to implement it but I also found a workaround by using Python to control the controls content. Here I publish this solution.

Data presentation

I've got the following tables:

  • On geographic table named OBJECTGEO.
  • Flat tables
    • One for AREAs
    • One for Couties
    • Last one for towns

The flat tables are linked together: each county has got a reference to an area (with an ID) and each town has got a reference to a county.

Here the schemas of all of the tables

Schema for OBJECTGEO

Schema for AREA

Schema for COUNTY

Schema for TOWN

Build the form

You can build the form on OBJECTGEO table just by following what is written below:

Forms for OBJECTGEO

Be sure to add value relations controls for the ID_AREA, ID_COUNTY and ID_TOWN fields:

An example of value-relation form control for ID_TOWN

For the moment, your form looks like this:

Form without cascade control

You can find that the values of the form are not linked together (if you are French you know that Finistère is not in Pays-de-la-loire and that SAUMUR is not in Finistère). Time to change this...

Write some Python code

Now that you have some controls in your form, you need to add a little bit of logic in order to have cascading controls. Everything is made in the following Python code.

# -*- encoding: utf-8 -*-

from PyQt4.QtCore import *
from PyQt4.QtGui import *

myDialog = None

def formOpen(dialog,layerid,featureid):
    global myDialog
    myDialog = dialog

    # We need to introspect the form
    # In QGis v2.4 autobuild form controls do not have a name... (fixed in v2.6)
    # So we need to retrieve form controls by field order !
    lst_children = dialog.findChildren(QComboBox)
    area = lst_children[0]
    county = lst_children[1]
    town = lst_children[2]

    # Clear the children QComboBox widgets
    county.clear()
    town.clear()

    # When you change the value for area widget, change the content of the county widget
    area.currentIndexChanged.connect(lambda: cascadeManage('COUNTY', 'ID_COUNTY', 'COUNTY', 'ID_AREA', county, area))
    # When you change the value for county widget, change the content of the town widget
    county.currentIndexChanged.connect(lambda: cascadeManage('TOWN', 'ID_TOWN', 'TOWN', 'ID_COUNTY', town, county))

def cascadeManage(layername, id_idx, txt_idx, fltr, widget, parent_widget):
    '''Generic function to manage cascading QComboBox.
       * layername: the layer (flat table) to request values for this control
       * id_idx: primary key for the table (used in for the form control key)
       * txt_idx: field shown in the form control QComboBox
       * fltr: field which is used to filter the content of this widget based on the parent widget value
       * widget: widget object (QComboBox) to control
       * parent_widget: get the value from the parent QComboBox widget'''
    from qgis.core import QgsMapLayerRegistry, QgsExpression

    # Get the layer (flat table)
    layers = QgsMapLayerRegistry.instance().mapLayersByName(layername)
    if (len(layers) > 0):
        layer = layers[0]
    else:
        return False

    # get attributes indexes
    ididx = layer.fieldNameIndex(id_idx)
    txtidx = layer.fieldNameIndex(txt_idx)

    # Get the value of the parent QComboBox
    filter_code = str(parent_widget.itemData(parent_widget.currentIndex()))

    # Build an expression with it
    exp = QgsExpression("{0} = {1}".format(fltr, filter_code))
    exp.prepare(layer.pendingFields())

    # clear the current widget content
    widget.clear()

    # And fill it with filtered data:
    for feature in layer.getFeatures():
        value = exp.evaluate(feature)
        if exp.hasEvalError():
            raise ValueError(exp.evalErrorString())
        if bool(value):
            widget.addItem(feature.attributes()[txtidx], feature.attributes()[ididx])

You can find explanations directly in the code...

In action

Save the upper code in a file named MYFORM.py in the same directory than your QGis project.

Now, make the link between the form and the code: change the value of Python Init function text with MYFORM.formOpen:

  • MYFORM refers to MYFORM.py Python file.
  • formOpen is the name of the method which will be launched when the form opens.

Add Python init function

Now, you can have actives cascading controls:

Cascading controls form in action

Whenever you choose an area, it restricts the list of counties. Same thing with counties and towns...

Beware, there is a bug in QGis 2.4 which just duplicate the new object when you use a form with Python code (you can find twice on the attribute table). But it is already fixed in the master version of QGis which will become v2.6 in november 2014. For QGis 2.4, you will just delete the duplicate entry in the attribute table.

Conclusion

As a conclusion, we have a mean to implement cascading form controls even (and especially) with value-relation controls. The code could be greatly improved. For example, with the 2.6 version of QGis, you can use field name instead of trying to find the good widgets sorted by order. You could go a bit deeper and make your own ui form (with QtCreator) and create cascading form controls that only modify the value of one field (in our case, we add AREA and COUNTY fields in OBJECTGEO but the real information we want is the ID of the TOWN).

I hope that QGis will provide soon an intern (and easier to use without Python code) method to use cascading form controls !

Posted jeu. 02 oct. 2014 19:14:07

Introduction

Après avoir réalisé quelques traductions sur systemd, il est temps de mettre vraiment les mains dans le cambouis, à savoir, de l'installer pour de bon sur un maximum de machines. Toutes mes machines fonctionnent sous Debian. Cet article a pour objet de présenter un passage de SysV à systemd comme démon de démarrage sur une machine sous Debian Jessie (testing au moment de la rédaction). Bien entendu, l'objectif est d'avoir une machine à iso-fonctionnalités, j'irai donc un peu plus loin que la simple configuration du démarrage de la machine et je vise à rendre tous mes services actifs comme sous SysV.

Depuis quelques temps déjà, j'ai des messages d'erreur à cause de systemd sur mes machines en Debian Jessie. En effet, au cours de l'été, des paquets systemd se sont installés par défaut dans Jessie. Depuis, il me manque peu de choses pour que tout systemd soit accessible...

# systemctl status
Failed to get D-Bus connection: No connection to service manager.

Indique que les binaires de systemd sont actifs mais que le démon systemd n'a pas été utilisé pour démarrer la machine. On peut régler le problème assez rapidement en installant le paquet systemd-sysv. Celui-ci permet simplement de remplacer /sbin/init par le binaire de systemd. Il n'est pas vraiment conseillé de le lancer tel quel. En effet, il y a de fortes chances pour que le passage à systemd ne se passe pas sans casser quelques services. Il vaut donc mieux procéder méthodiquement.

Le tour du propriétaire

Avant de se lancer dans le passage à systemd, mieux vaut savoir quels sont les services disponibles et si ces derniers seront disponibles. Voici ce dont je dispose sur mon ordinateur portable:

  • Un serveur web (Apache 2.4)
  • Toute ma configuration sonore repose sur alsa. Je n'ai pas Pulseaudio.
  • Ma configuration réseau est inactive par défaut. Elle repose sur /etc/network/interfaces.
  • Xorg utilise un pilote radeon libre.
  • J'utilise VirtualBox pour lancer quelques VMs au cas où. Il utilise un module noyau configuré via DKMS.
  • Mon écran de login est slim qui redirige vers i3-wm.
  • J'ai une configuration ACPI très customisée pour ce portable (un Lenovo e145).
  • J'ai un service rsyslog configuré par défaut (la conf du mainteneur du paquet Debian).
  • J'utilise un partitionnement particulier:
    • une partition / qui contient le système
    • une partition /media/data qui contient des data
    • un /home/user chiffré, monté avec pam_mount
  • Pour accélérer mon démarrage, j'utilise e4rat. Ce dernier se substitue au système d'Init pour précharger des fichiers avant de rendre la main à Init.

Rien de bien affriolant mais il y a sans doute quelques points qui pourront poser quelques problèmes.

Lancer systemd

Si vous utilisez Debian Jessie depuis quelques temps, vous avez dû vous rendre compte que systemd est déjà installé, même si vous n'avez rien demandé et que vous vous êtes contentés de faire des aptitude safe-upgrade. En effet, comme systemd sera le système d'Init par défaut, il sera installé. A l'heure de la rédaction de cet article, c'est systemd v215 qui équipe ma machine. Une version assez récente et qui offre de nombreuses fonctionnalités et dont on attend assez de stabilité. Voici ce qui est installé par défaut:

# aptitude search systemd | grep ^i
i A libpam-systemd                  - system and service manager - PAM module
i A libsystemd0                     - systemd utility library
i A systemd                         - gestionnaire système et service
i   systemd-shim                    - shim for systemd

Pour tester systemd, rien de plus facile: il faut indiquer au bootloader qu'on souhaite l'utiliser comme système d'init en ajoutant une variable nommé init à la fin de la ligne de chargement du noyau Linux dans Grub:

linux   /boot/vmlinuz-3.16-2-amd64 root=UUID=7840a396-1ab7-4acb-869c-083d607baa0d ro  quiet init=/bin/systemd

Premier lancement sous systemd

Lors du premier boot, je peux constater que systemd lance d'autres services que ceux dont j'ai l'habitude de voir apparaître. Il s'agit essentiellement des services de l'éco-système systemd comme systemd-hostname ou systemd-journald. Pour l'instant, on s'en balance, le truc est d'avoir un système iso-fonctionnel.

Faisons donc la liste de ce qui marche:

  • Xorg est lancé !
  • Mon lanceur de sessions graphiques (slim) se lance correctement !
  • Mon gestionnaire de windows fonctionne correctement !
  • Mon répertoire home chiffré est correctement accessible !
  • Apache est bien lancé et j'ai accès à ses services !
  • Ma carte son sous Alsa fonctionne correctement !
  • Mes machines virtuelles sont accessibles via VirtualBox et le module noyau spécifique est bien fonctionnel.
  • rsyslog capte bien des logs même si journald semble actif...
  • Globalement, ça boote plus rapidement (gain d'environ 6 secondes comparé à un système sans e4rat).
  • La mise en veille (suspend to ram) est bien plus rapide qu'avant (divisée par deux).
  • L'arrêt via le bouton est beaucoup plus rapide qu'avant (divisée par deux).

Néanmoins, tout n'est pas rose:

$ systemctl status
trick
   State: degraded
    Jobs: 0 queued
  Failed: 1 units

De plus, quelques-unes de mes touches spéciales, configurées via ACPI ne fonctionnent plus. Il s'agit des touches pour régler la luminosité et de celle qui active le wifi. Néanmoins l'accès au réseau est possible via le classique ifup.

Corrections

Essayons de voir ce qui plante...

# systemctl --failed
  UNIT                                            LOAD   ACTIVE SUB    DESCRIPTION
● systemd-backlight@backlight:acpi_video0.service loaded failed failed Load/Save Screen Backlight Brightness of backlight:acpi_video0

La seule unité systemd qui ne fonctionne pas est liée à un problème de configuration du contrôle de la luminosité de mon ordinateur portable. Le problème se situe au niveau du noyau qui ne gère pas correctement cet élément. C'est un problème connu et que j'ai toujours ignoré jusqu'à présent car j'ai d'autres moyens pour gérer la luminosité sur cette machine. On peut donc dégager cette unité qui ne me sert pas.

# systemctl mask systemd-backlight@backlight:acpi_video0.service

Reste ensuite, le problème des touches ACPI. Sur cette machine, j'ai une batterie de scripts spécifiques qui sont activés lors de la frappe sur les touches situées en haut du clavier du Lenovo e145. Après le passage à systemd, j'ai pu remarquer que ces touches ne fonctionnent plus. En fait, en allant plus loin, on peut remarquer qu'il me suffit de lancer une opération qui a besoin de l'ACPI pour qu'elles se remettent à fonctionner. C'est le cas lorsque je lance l'utilitaire acpi_listen.

Ce comportement m'oriente vers un problème lié à l'activation par socket de systemd. On dirait que le service ne se lance pas normalement. En revanche dès qu'un programme s'adresse à la socket, le service est à nouveau opérationnel. Il doit donc y avoir un problème d'activation de socket lors de l'appui initial d'une touche ACPI.

Après avoir passé quelques temps à chercher une solution sur Internet, je me suis dit qu'il fallait plus simplement activer acpid par défaut et qu'il y avait sans doute un élément de conf non prise en charge par systemd. J'ai donc tout simplement manuellement activé acpid.socket et acpid.service:

# systemctl enable acpid.socket
# systemctl enable acpid.service
Synchronizing state for acpid.service with sysvinit using update-rc.d...
Executing /usr/sbin/update-rc.d acpid defaults
Executing /usr/sbin/update-rc.d acpid enable

Après reboot, mes touches ACPI redeviennent fonctionnelles !

configuration de e4rat

e4rat permet d'accélérer le processus de boot en préchargeant un ensemble de fichiers en RAM. Il semble compatible avec systemd. Vous aurez besoin de relancer une opération de collecte car il est évident que le passage à systemd implique de précharger les fichiers relatifs à systemd (les binaires des démons notamment). Vous devrez donc booter votre machine avec e4rat-collect (cf la doc de e4rat pour plus de détails sur cette opération).

Une fois la collecte terminée, il reste à indiquer à e4rat de passer la main systemd à la place du binaire Init classique. Vous pouvez le faire en modifiant le fichier /etc/e4rat.conf en modifiant la variable init:

; path to init process binary (DEFAULT: /sbin/init)
init /bin/systemd

Une fois activée, cette option vous permettra de démarrer avec systemd comme d'habitude. Le processus de boot sera encore accéléré, vous bénéficierez à la fois du préchargement des fichiers et de la forte parralélisation de systemd.

Conclusion

On peut voir dans cet article que le passage à systemd sous Debian Jessie se réalise plutôt facilement. Il y a finalement assez peu de modifications de la configuration pour obtenir un système iso-fonctionnel à celui qui démarre avec sysvinit. C'est plutôt encourageant pour la suite et montre qu'un bon travail de configuration a été mené en amont par les empaqueteurs de systemd sous Debian.

Néanmoins, le cas que nous avons pu étudié reste relativement simple. En effet, dans notre exemple, il y a peu de services spécifiques et la majorité des unités systemd qui sont lancées sont génériques et relatives à l'éco-système systemd. Pour aller plus loin, on pourrait se lancer dans la conversion d'une machine qui disposerait de davantage de services réseau. Un vrai serveur serait une cible d'étude d'intérêt et révèlerait sans doute plus d'adaptation de configuration.

Dans tous les cas, plus il y aura d'administrateurs systèmes qui tenteront le passage à systemd, plus nous aurons de retour d'expérience sur le sujet et plus facilement nous pourrons sortir des impasses techniques amenées par systemd... A vous de jouer !

Posted mar. 07 oct. 2014 19:04:00

Introduction

Ikiwiki permet d'éditer des pages en ligne. Jusqu'à présent, j'utilisais uniquement Git pour commiter des articles sur ma plate-forme Ikiwiki. C'est un moyen assez efficace pour publier des articles car j'utilise mon éditeur de texte favori (Emacs) pour le faire et ça ne pose aucun problème.

Néanmoins, parfois, on a besoin d'effectuer juste une petite correction et le faire dans un navigateur Web reste sans doute le moyen le plus effiace. Mais qui dit édition en ligne, dit sécurité ! Il ne faut pas en effet que n'importe qui puisse réaliser ces modifications sans contrôle.

Mon cas d'utilisation est assez simple: il n'y a qu'un seul utilisateur qui publie sur la plate-forme. Je n'ai donc pas besoin de mettre en place un système complexe basé sur une base de données. De plus, j'aimerais bien pouvoir réutiliser une des méthodes d'authentification dont je me sers fréquemment.

Ces besoins impliquent d'utiliser un plugin particulier d'Ikiwiki qui se nomme httpauth. Ce dernier permet de déléguer le mécanisme d'authentification au serveur Web. En cas d'authentification réussie, ce dernier passe la main au CGI d'Ikiwiki avec les bons paramètres.

Cet article tente d'illustrer la configuration de ce plugin dans Ikiwiki ainsi que la mise en place du mécanisme d'authentification de manière à disposer d'une édition en ligne sécurisée.

Installation des paquets nécessaires à l'authentification

Pour faire fonctionner le plugin httpauth, il est nécessaire de disposer des bons modules Perl. En effet, le CGI d'Ikiwiki ne permet pas grand chose sans les bons paquets. Si vous tentez d'utiliser le plugin httpauth sans ces modules, le CGI retournera une erreur.

# aptitude install libcgi-session-perl libcgi-formbuilder-perl

Avec ces modules, le CGI d'Ikiwiki est capable de générer des formulaires web ainsi que de gérer des sessions. C'est tout ce dont nous avons besoin à ce niveau.

Configuration d'Ikiwiki

si vous consultez la documentation d'Ikiwiki sur l'authentification, vous pourrez constater qu'il y a plusieurs méthodes autres que httpauth. Par défaut, Ikiwiki utilise passwordauth. Ce dernier créé une base de données utilisateur (dans le répertoire .ikiwiki/userdb) qui gère les accès. Par défaut, cette base est ouverte, c'est à dire qu'on peut s'y enregistrer simplement en remplissant un formulaire. En conclusion, si vous laissez Ikiwiki sans modifier sa configuration, n'importe qui pourra créer et modifier des articles en créant un compte bidon. Voilà pourquoi il faut enchaîner toute de suite avec la configuration du plugin httpauth.

Vous devez modifier votre fichier de configuration Ikiwiki (fichier .setup). Voici ce que j'ai fait au mien:

    ...
    # plugins to add to the default configuration
    add_plugins => [qw{goodstuff format highlight toggle img rawhtml favicon sidebar meta calendar search editpage httpauth}],
    # plugins to disable
    disable_plugins => [qw{passwordauth openid anonok}],
    ...
    # httpauth plugin
    # url to redirect to when authentication is needede
    cgiauthurl => 'https://example.org/auth/ikiwiki.cgi',
    ...

J'ai tout simplement ajouté le plugin editpage pour permettre l'édition de pages ainsi que le plugin httpauth pour la gestion de l'authentification. Ensuite, j'ai préventivement désactivé tous les autres plugins liés à l'authentification (passwordauth, openid et anonok).

Enfin, j'ai renseigné une variable nommée cgiauthurl. Cette dernière permet au CGI d'Ikiwiki d'être accédé depuis une autre URL lorsqu'aucune session valide n'a été repérée. C'est ce mécanisme qui va nous permettre de gérer l'authentification. Il suffira de déclencher une authentification lorsqu'un utilisateur désire accéder à https://example.org/auth/ et le tour sera joué. Remarquez que cette page est accédée via TLS (HTTPS) ce qui n'est pas un hasard.

Vous aurez sans doute besoin de créer le répertoire défini par cgiauthurl. Dans mon cas, j'ai juste créé le répertoire /var/www/auth avec les bons droits (ceux pour www-data) et réalisé un lien symbolique de /var/www/ikiwiki vers /var/www/auth/ikiwiki.cgi:

# mkdir /var/www/auth
# chown www-data:www-data /var/www/auth
# ln -s /var/www/ikiwiki.cgi /var/www/auth/ikiwiki.cgi

Il faudra bien sûr adapter ces chemins à votre installation d'Ikiwiki.

Pour prendre en compte ce changement de configuration vous aurez besoin de regénérer le CGI d'Ikiwiki. Cette opération est déclenchée lorsque vous reconstruisez l'ensemble du site:

$ ikiwiki --setup ikiwiki.setup

Configuration Apache

Maintenant que nous avons configuré Ikiwiki, il reste à s'occuper d'Apache. En effet, c'est bien le serveur HTTP qui va réaliser une authentification de type Basic et passer la main au CGI d'Ikiwiki si l'authentification s'est correctement déroulée. Le serveur Web envoie le nom de l'utilisateur authentifié au CGI via une variable d'environnement, nommée REMOTE_USER.

Puisque l'authentification se fera en mode Basic, une connexion chiffrée est indispensable ! C'est à cet effet que nous avons délibérément configuré la variable Ikiwiki cgiauthurl en https. Ainsi, lorsqu'un utilisateur tente d'éditer une page en mode HTTP standard, il est automatiquement redirigé vers la partie HTTPS du site et aucun mot de passe ne transitera en clair sur le réseau.

Il faut donc éditer cette partie au niveau de la configuration d'Apache pour déclencher une authentification. Je ne vais pas rentrer dans les détails car votre configuration peut être très différente de la mienne. Dans mon cas, j'ai juste défini un nouvel emplacement dans la configuration du VirtualHost dédié à HTTPS:

    # Ikiwiki authentication
    <Directory /var/www/auth/>
        Options -Indexes FollowSymLinks MultiViews +ExecCGI
        AllowOverride None
        Order allow,deny
        Allow from all
        AuthType Basic
        AuthName "Ikiwiki authentication"
        AuthUserFile /etc/apache2/my_userdb
        Require valid-user
    </Directory>

Attention, le CGI d'Ikiwiki utilise la variable d'environnement REMOTE_USER retournée par Apache pour déterminer le nom de l'utilisateur authentifié. Si vous avez défini cette variable (avec un SetEnv par exemple) dans votre configuration Apache et même si cette valeur est nulle, Ikiwiki l'utilisera et considérera que l'utilisateur est authentifié. Dans mon cas, j'avais un comportement assez particulier: Ikiwiki ne me demandait jamais de m'authentifier et le nom de l'utilisateur pour la session était vide. J'avais tout simplement initialisé la variable d'environnement REMOTE_USER par défaut car j'avais mis en place sur mon serveur un accès Git par HTTP (git-http-backend). Ce dernier définissait la variable de la manière suivante:

    SetEnv REMOTE_USER=$REDIRECT_REMOTE_USER

Lorsque j'accède à mes dépôts Git via HTTP(S), le client Git remonte la variable REDIRECT_REMOTE_USER. Mais lorsqu'on n'utilise pas Git (ce qui est le cas lorsqu'on édite une page avec le CGI d'Ikiwiki), la variable d'environnement REDIRECT_REMOTE_USER est vide. Cette configuration Apache créé la variable REMOTE_USER, peu importe le cas. Cette définition est trop générale (et risquée).

A la place, il faut définir autrement cette variable dans la configuration Apache:

    SetEnvIf Request_URI "^/git/" REMOTE_USER=$REDIRECT_REMOTE_USER

Dans ce cas, la variable REMOTE_USER n'est définie que pour les URLs qui commencent par /git/ (ce qui n'est pas le cas lorsqu'on édite une page avec le CGI d'Ikiwiki). Ne pas redéfinir cette variable en mode conditionnel conduit à un vrai trou de sécurité ! Alors faîtes bien attention à votre configuration Apache...

Conclusion

Dans cet article, on voit que la mise en place d'un système d'authentification est assez accessible. Il y a juste quelques modules Perl à installer ainsi qu'un minimum de configuration d'Ikiwiki et d'Apache. Je ne peux que vous recommander d'effectuer quelques tests en pré-production avant de valider la configuration car l'enjeu est important: si vous n'avez pas assez sécurisé cette partie, n'importe quel attaquant pourra vous pourrir vos articles (ou faire du DDOS).

Maintenant, je peux faire de l'édition en ligne de manière sécurisée. Cet article a d'ailleurs été complètement rédigé en ligne !

Posted ven. 17 oct. 2014 18:34:00