local idp (beta)

This commit is contained in:
Thomas NOËL 2015-01-26 18:01:44 +01:00
parent 4af9283c39
commit b62e4e4d33
63 changed files with 13333 additions and 0 deletions

View File

@ -0,0 +1,27 @@
<?php
/*
* privileges for univnautes IdP
*
*/
global $priv_list;
$priv_list['univnautes-idp'] = array();
$priv_list['univnautes-idp']['name'] = "UnivNautes IdP - account";
$priv_list['univnautes-idp']['descr'] = "Local IdP account";
$priv_list['univnautes-idp']['match'] = array();
$priv_list['univnautes-idp']['match'][] = "NOTHING";
$priv_list['univnautes-idp-multiple'] = array();
$priv_list['univnautes-idp-multiple']['name'] = "UnivNautes IdP - multiple connections";
$priv_list['univnautes-idp-multiple']['descr'] = "allowing multiple connections";
$priv_list['univnautes-idp-multiple']['match'] = array();
$priv_list['univnautes-idp-multiple']['match'][] = "NOTHING";
$priv_list['univnautes-idp-admin'] = array();
$priv_list['univnautes-idp-admin']['name'] = "UnivNautes IdP - administrator";
$priv_list['univnautes-idp-admin']['descr'] = "manage local IdP accounts";
$priv_list['univnautes-idp-admin']['match'] = array();
$priv_list['univnautes-idp-admin']['match'][] = "NOTHING";
?>

View File

@ -0,0 +1,23 @@
#!/bin/sh
COMMAND=update-metadatas
# lock to avoid concurrent updates
LOCK=/var/run/univnautes-idp-$COMMAND.lock
if [ -r $LOCK ]
then
PID=`cat $LOCK`
echo "$COMMAND locked by $LOCK"
ps waux | grep "$PID" | grep $COMMAND | grep -vq grep && exit
echo "... but PID $PID is not a $COMMAND, continue"
fi
unlock() {
rm -f $LOCK
exit
}
trap unlock INT TERM EXIT
echo $$ > $LOCK
cd /usr/local/univnautes/idp
./manage.py $COMMAND

View File

@ -0,0 +1,3 @@
# add_attributes_to_response signal
import add_attributes

View File

@ -0,0 +1,58 @@
from authentic2.idp.signals import add_attributes_to_response
import xml.etree.ElementTree as ET
from hashlib import sha1
from django.conf import settings
import syslog
def add_attributes(request, user, audience, **kwargs):
# <saml:Attribute Name="urn:oid:2.16.840.1.113730.3.1.241"
# NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
# FriendlyName="displayName">
# <saml:AttributeValue>Jean Dupont</saml:AttributeValue>
# </saml:Attribute>
displayName = ('urn:oid:2.16.840.1.113730.3.1.241',
'urn:oasis:names:tc:SAML:2.0:attrname-format:uri',
'displayName')
displayName_value = user.displayname
# <saml:Attribute Name="urn:oid:1.3.6.1.4.1.5923.1.1.1.10"
# NameFormat="urn:oasis:names:tc:SAML:2.0:attrname-format:uri"
# FriendlyName="eduPersonTargetedID">
# <saml:AttributeValue><saml:NameID
# Format="urn:oasis:names:tc:SAML:2.0:nameid-format:persistent"
# NameQualifier="https://services-federation.renater.fr/test/idp"
# SPNameQualifier="https://univnautes.entrouvert.lan/authsaml2/metadata">
# C8NQsm1Y3Gas9m0AMDhxU7UxCSI=
# </saml:NameID>
# </saml:AttributeValue>
# </saml:Attribute>
eduPersonTargetedID = ('urn:oid:1.3.6.1.4.1.5923.1.1.1.10',
'urn:oasis:names:tc:SAML:2.0:attrname-format:uri',
'eduPersonTargetedID')
pseudoid = sha1(user.username + audience + settings.SECRET_KEY).hexdigest()
eduPersonTargetedID_value = ET.Element('saml:NameID')
eduPersonTargetedID_value.attrib['Format'] = 'urn:oasis:names:tc:SAML:2.0:nameid-format:persistent'
eduPersonTargetedID_value.attrib['NameQualifier'] = request.build_absolute_uri('/idp/saml2/metadata')
eduPersonTargetedID_value.attrib['SPNameQualifier'] = audience
eduPersonTargetedID_value.attrib['xmlns:saml'] = 'urn:oasis:names:tc:SAML:2.0:assertion'
eduPersonTargetedID_value.text = pseudoid
privileges = getattr(user, 'univnautes_privileges', [])
syslog.openlog("idpauth", syslog.LOG_PID)
syslog.syslog(syslog.LOG_LOCAL4 | syslog.LOG_INFO,
'send user:%s to %s with eduPersonTargetedID:%s, privileges %s' %
(user.username, audience, pseudoid, privileges))
return { 'attributes': {
displayName: [displayName_value],
eduPersonTargetedID: [eduPersonTargetedID_value],
'univnautesPrivileges': privileges,
} }
add_attributes_to_response.connect(add_attributes)

View File

@ -0,0 +1,87 @@
from django.contrib.auth.models import SiteProfileNotAvailable
from authentic2.authsaml2.models import SAML2TransientUser
import subprocess
import syslog
#
# Django auth backend
#
class PfBackend:
def authenticate(self, username=None, password=None):
pfuser, privileges = pf_authenticate_user(username, password)
if not pfuser:
syslog.openlog("idpauth", syslog.LOG_PID)
# FIXME: add details:
# does not exists, bad password, expired account... ?
syslog.syslog(syslog.LOG_LOCAL4 | syslog.LOG_INFO ,
"FAIL: bad user/pass for %s" % username)
return None
if not 'univnautes-idp' in privileges:
syslog.openlog("idpauth", syslog.LOG_PID)
syslog.syslog(syslog.LOG_LOCAL4 | syslog.LOG_INFO ,
"FAIL: user %s do not have IdP privilege" % username)
return None
return TransientUser(pfuser, privileges)
def get_user(self, user_id=None):
pfuser, privileges = pf_get_user(user_id)
if not pfuser or not 'univnautes-idp' in privileges:
return None
return TransientUser(pfuser, privileges)
class TransientUser(SAML2TransientUser):
'''mimics a django user'''
is_active = True
is_staff = False
univnautes_privilegess = tuple()
def __init__(self, pfuser, privileges):
self.id = pfuser['username']
self.pk = self.id
self.displayname = pfuser['displayname'].decode('latin1')
self.univnautes_privileges = privileges
if privileges and 'univnautes-idp-admin' in privileges:
self.is_staff = True
def __unicode__(self):
return u'%s (%s)' % (self.displayname, self.username)
def get_profile(self):
raise SiteProfileNotAvailable
def get_username(self):
return self.id
username = property(get_username)
#
# get user from pfSense, via PHP script ../univnautes/bin/pf_auth
#
def pf_authenticate_user(username, password):
params = ['username=%s' % username, 'password=%s' % password]
return pf_auth(params)
def pf_get_user(username):
params = ['username=%s' % username]
return pf_auth(params)
def pf_auth(params):
cmd = ['/usr/local/univnautes/idp/idp/pf_auth', ] + params
try:
p = subprocess.Popen(cmd, close_fds=True,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
except OSError, e:
syslog.openlog("idpauth", syslog.LOG_PID)
syslog.syslog(syslog.LOG_LOCAL4 | syslog.LOG_INFO , "ERROR: OSError %s" % e)
return None, None
stdout, stderr = p.communicate()
if p.returncode != 0:
return None, None
user, privileges = eval(stdout)
return user, privileges

View File

@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
#
# UnivNautes
# Copyright (C) 2014 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.core.management.base import BaseCommand, CommandError
import os
import sys
from idp import pfconfigxml
def get(key, args):
if key == 'idp':
idp = pfconfigxml.get_idp()
if idp is None:
sys.exit(1)
else:
print idp
class Command(BaseCommand):
help = 'get infos from config.xml'
def handle(self, *args, **options):
try:
action, key = args[0], args[1]
except IndexError:
print "syntax: configxml get|set key"
return
if action == 'get':
return get(key, args)

View File

@ -0,0 +1,139 @@
# -*- coding: utf-8 -*-
#
# UnivNautes
# Copyright (C) 2014 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from django.core.management.base import BaseCommand, CommandError
from django.core.management import call_command
import os
import sys
import subprocess
from idp import pfconfigxml
import urllib2
from authentic2.saml.models import LibertyProvider
from django.conf import settings
METADATAS_DIR = os.path.join('/', 'var', 'db', 'univnautes-idp-federations')
def metadata_filename(codename, suffix=None):
if not suffix:
return os.path.join(METADATAS_DIR, codename, 'metadata.xml')
else:
return os.path.join(METADATAS_DIR, codename, 'metadata.xml.%s' % suffix)
def store_metadata(codename, metadata):
tempfilename = '/var/tmp/idp_%s-metadata.xml-%s' % (codename, os.getpid())
f = open(tempfilename, 'wb')
f.write(metadata)
f.close()
if not os.path.exists(METADATAS_DIR):
os.mkdir(METADATAS_DIR, 0770)
dir = os.path.join(METADATAS_DIR, codename)
if not os.path.exists(dir):
os.mkdir(dir, 0770)
os.rename(tempfilename, metadata_filename(codename, 'downloaded'))
# remove obsoletes files if they exist (just to be clean...)
for suffix in ('bad_signature', 'disabled'):
try:
os.remove(metadata_filename(codename, suffix))
except OSError:
pass
def verify_metadata(codename, signcert):
metadata = metadata_filename(codename, 'downloaded')
if not signcert:
print 'warn: do not verify %s metadata (no certificate provided)' % codename
ret = True
else:
signcert_pem = metadata_filename(codename, 'signcert.pem')
dir = os.path.join(METADATAS_DIR, codename)
f = open(signcert_pem, 'wb')
f.write(signcert)
f.close()
ret = 0 == subprocess.call(['xmlsec1', '--verify',
'--id-attr:ID', 'EntitiesDescriptor',
'--pubkey-cert-pem', signcert_pem,
'--enabled-key-data', 'key-name',
metadata])
if ret:
os.rename(metadata, metadata_filename(codename))
else:
print 'warn: bad signature for %s metadata' % codename
os.rename(metadata, metadata_filename(codename, 'bad_signature'))
return ret
def disable(codename):
print 'disable %s metadata' % str(codename)
LibertyProvider.objects.filter(federation_source=codename).delete()
dir = os.path.join(METADATAS_DIR, codename)
try:
os.rename(metadata_filename(codename), metadata_filename(codename, 'disabled'))
except OSError:
pass
class Command(BaseCommand):
help = 'update all metadatas in %s' % METADATAS_DIR
def handle(self, *args, **options):
actives = set()
federations = pfconfigxml.get_federations()
for federation in federations:
url = federation.get('url')
metadata = federation.get('metadata')
codename = federation.get('codename')
descr = federation.get('descr')
signcert = federation.get('signcert')
if not metadata:
# get metadata from url
try:
print 'download federation %s metadata from url: %s' % (str(codename), str(url))
metadata = urllib2.urlopen(url).read()
except urllib2.HTTPError as e:
metadata = None
print 'Error loading metadata (%s)' % str(e)
except urllib2.URLError as e:
metadata = None
print 'Error loading metadata (%s)' % str(e)
else:
print 'using local metadata for %s' % str(codename)
if metadata:
store_metadata(codename, metadata)
if verify_metadata(codename, signcert):
try:
call_command('sync-metadata', metadata_filename(codename),
source=codename, sp=True, idp=False)
except Exception, e:
print 'sync-metadata aborted: %s' % str(e)
print 'can not active metadata for %s' % str(codename)
else:
actives.add(codename)
present_in_filesystem = set(os.listdir(METADATAS_DIR))
for codename in present_in_filesystem - actives:
disable(codename)
present_in_database = set([str(i['federation_source']) \
for i in LibertyProvider.objects.distinct().values('federation_source').order_by('federation_source')])
for codename in present_in_database - actives:
disable(codename)

View File

@ -0,0 +1,63 @@
#!/usr/local/bin/php -f
<?php
/*
* pf_auth username=.... : return a "python-formatted" user and its privileges:
* ({'username':...,'displayname':....}, ('priv1','priv2',...))
* with password=.... : only return the user if authenticate on pfsense
*
*/
array_shift($argv);
foreach ($argv as $arg) {
list($name, $value) = explode('=', $arg, 2);
switch($name) {
case 'username':
$username=$value;
break;
case 'password':
$password=$value;
break;
default:
echo "BAD SYNTAX\n";
exit(2);
}
}
require("auth.inc");
if (isset($password)) {
if (!authenticate_user($username, $password)) {
echo "AUTH FAIL\n";
exit(1);
}
}
$user = getUserEntry($username);
if (!$user) {
echo "UNKNOWN USER\n";
exit(3);
}
$k = array(
"name" => "username",
"descr" => "displayname",
);
echo "({";
foreach($k as $key => $attr) {
echo '"' . $attr . '":"""' . $user[$key] . '""", ';
}
echo "},";
$privs = get_user_privileges($user);
if ($privs) {
echo "(";
foreach($privs as $val) {
echo "'$val', ";
}
echo ")";
} else {
echo "None";
}
echo ")";
exit(0);
?>

View File

@ -0,0 +1,138 @@
# -*- coding: utf-8 -*-
#
# UnivNautes
# Copyright (C) 2014 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import xml.etree.ElementTree as ET
from base64 import b64decode
from django.conf import settings
def root():
with open(settings.CONFIG_XML,'r') as f:
x = ET.fromstring(f.read())
return x
def laxint(s):
try:
return int(s)
except:
return 0
def get_ca(caref):
for ca in root().findall('ca'):
if ca.find('refid').text == caref:
crt = ca.find('crt')
if crt is not None:
crt = crt.text.decode('base64')
prv = ca.find('prv')
if prv is not None:
prv = prv.text.decode('base64')
return {
'caref': caref,
'crt': crt,
'prv': prv,
}
return None
def get_cert(certref):
for cert in root().findall('cert'):
if cert.find('refid').text == certref:
crt = cert.find('crt')
if crt is not None:
crt = crt.text.decode('base64')
prv = cert.find('prv')
if prv is not None:
prv = prv.text.decode('base64')
caref = cert.find('caref')
if caref is not None:
caref = caref.text
descr = cert.find('descr')
if descr is not None:
descr = descr.text
return {
'certref': caref,
'descr': descr,
'caref': caref,
'crt': crt,
'prv': prv,
}
return None
def get_idp():
idp = root().find('univnautes/idp')
if idp is None:
return None
if idp.find('enable') is None:
return None
certref = idp.find('certref')
if certref is not None:
saml_cert = get_cert(certref.text)
else:
saml_cert = None
return { 'saml_cert': saml_cert }
def get_federations():
"""
<federation>
<enable/>
<codename>renater_test</codename>
<refid>fed_53d1161955a26</refid>
<descr><![CDATA[Renater TEST Federation]]></descr>
<url>https://federation.renater.fr/test/renater-test-metadata.xml</url>
<metadata>[base64 encoded metadata]</metadata>
<certref>53d115fac567b</certref>
</federation>
"""
xml_federations = root().find('univnautes/federations')
if xml_federations is None:
return []
xml_federations = xml_federations.findall('federation')
federations = []
for xml_federation in xml_federations:
if xml_federation.find('enable') is None:
continue
codename = xml_federation.find('codename')
if codename is not None:
codename = codename.text
url = xml_federation.find('url')
if url is not None:
url = url.text
metadata = xml_federation.find('metadata')
if metadata is not None:
try:
metadata = b64decode(metadata.text)
except:
metadata = None
descr = xml_federation.find('descr')
if descr is not None:
descr = descr.text
certref = xml_federation.find('certref')
if certref is not None:
signcert = (get_cert(certref.text) or {}).get('crt')
else:
signcert = None
federations.append({
'codename': codename,
'url': url,
'metadata': metadata,
'signcert': signcert,
'descr': descr,
})
return federations

View File

@ -0,0 +1,252 @@
# Django settings for idp project.
import os
import pfconfigxml
from django.conf import global_settings
PROJECT_PATH = os.path.dirname(os.path.dirname(__file__))
DEBUG = os.environ.get('DEBUG') == 'yes'
TEMPLATE_DEBUG = DEBUG
# fastcgi (see http://docs.djangoproject.com/en/dev/howto/deployment/fastcgi/)
FORCE_SCRIPT_NAME=''
ADMINS = ()
MANAGERS = ADMINS
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.sqlite3',
'NAME': '/var/db/univnautes-idp.sqlite3',
}
}
# Hosts/domain names that are valid for this site; required if DEBUG is False
# See https://docs.djangoproject.com/en/1.5/ref/settings/#allowed-hosts
ALLOWED_HOSTS = ['*']
# Local time zone for this installation. Choices can be found here:
# http://en.wikipedia.org/wiki/List_of_tz_zones_by_name
# although not all choices may be available on all operating systems.
# In a Windows environment this must be set to your system time zone.
TIME_ZONE = 'Europe/Paris'
# Language code for this installation. All choices can be found here:
# http://www.i18nguy.com/unicode/language-identifiers.html
LANGUAGE_CODE = 'fr-fr'
SITE_ID = 1
# If you set this to False, Django will make some optimizations so as not
# to load the internationalization machinery.
USE_I18N = True
# If you set this to False, Django will not format dates, numbers and
# calendars according to the current locale.
USE_L10N = True
# If you set this to False, Django will not use timezone-aware datetimes.
USE_TZ = True
# Absolute filesystem path to the directory that will hold user-uploaded files.
# Example: "/var/www/example.com/media/"
MEDIA_ROOT = ''
# URL that handles the media served from MEDIA_ROOT. Make sure to use a
# trailing slash.
# Examples: "http://example.com/media/", "http://media.example.com/"
MEDIA_URL = ''
# Absolute path to the directory static files should be collected to.
# Don't put anything in this directory yourself; store your static files
# in apps' "static/" subdirectories and in STATICFILES_DIRS.
# Example: "/var/www/example.com/static/"
STATIC_ROOT = os.path.join(PROJECT_PATH, 'www', 'static')
# URL prefix for static files.
# Example: "http://example.com/static/", "http://static.example.com/"
STATIC_URL = '/static/'
# Additional locations of static files
STATICFILES_DIRS = (
# Put strings here, like "/home/html/static" or "C:/www/django/static".
# Always use forward slashes, even on Windows.
# Don't forget to use absolute paths, not relative paths.
)
# List of finder classes that know how to find static files in
# various locations.
STATICFILES_FINDERS = (
'django.contrib.staticfiles.finders.FileSystemFinder',
'django.contrib.staticfiles.finders.AppDirectoriesFinder',
)
# Make this unique, and don't share it with anybody.
SECRET_KEY_FILENAME = os.path.join(PROJECT_PATH, 'secret.key')
try:
with open(SECRET_KEY_FILENAME, 'rb') as sk:
SECRET_KEY = sk.read()
except IOError:
import random, string
SECRET_KEY = "".join([random.SystemRandom().choice(string.digits + string.letters + string.punctuation) for i in range(100)])
with open(SECRET_KEY_FILENAME, 'wb') as sk:
sk.write(SECRET_KEY)
# List of callables to import templates from various sources.
TEMPLATE_LOADERS = (
'idp.template_loader.Loader',
'django.template.loaders.filesystem.Loader',
'django.template.loaders.app_directories.Loader',
)
MIDDLEWARE_CLASSES = (
'django.middleware.common.CommonMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'authentic2.idp.middleware.DebugMiddleware'
)
ROOT_URLCONF = 'idp.urls'
# Python dotted path to the WSGI application used by Django's runserver.
WSGI_APPLICATION = 'idp.wsgi.application'
CPELEMENTS = '/var/db/cpelements'
TEMPLATE_DIRS = (
CPELEMENTS,
os.path.join(PROJECT_PATH, 'idp', 'templates'),
)
INSTALLED_APPS = (
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.sites',
'django.contrib.messages',
'django.contrib.staticfiles',
'authentic2.saml',
'authentic2.idp',
'authentic2.attribute_aggregator',
'authentic2.idp.saml',
'authentic2.auth2_auth',
'idp',
'idp.users_admin',
)
if DEBUG:
INSTALLED_APPS += ('django.contrib.admin',)
SESSION_SERIALIZER = 'django.contrib.sessions.serializers.PickleSerializer'
# it can't be 'django.contrib.sessions.serializers.JSONSerializer' with authentic2 (attributes)
SESSION_COOKIE_NAME = 'univnautes-idp-sessionid'
SESSION_ENGINE = 'django.contrib.sessions.backends.file'
SESSION_FILE_PATH = '/var/tmp/univnautes-idp-sessions'
SESSION_EXPIRE_AT_BROWSER_CLOSE = True
SESSION_COOKIE_AGE = 36000 # one day of work
SESSION_COOKIE_NAME = "pfidpsessionid"
try:
os.mkdir(SESSION_FILE_PATH)
except:
pass
MESSAGE_STORAGE = 'django.contrib.messages.storage.cookie.CookieStorage'
LOGIN_REDIRECT_URL = '/'
LOGIN_URL = '/login'
# logging configuration
# FIXME : syslog (freebsd -> /var/run/log) / local4 / debug
LOGGING = {
'version': 1,
'disable_existing_loggers': False,
'handlers': {
'tmpidpfile': {
'level': 'DEBUG',
'class': 'logging.FileHandler',
'filename': '/tmp/idp.log'
},
},
'loggers': {
'django.request': {
'handlers': ['tmpidpfile'],
'level': 'DEBUG',
'propagate': True,
},
}
}
# authentic2 settings (IDP)
# SAML2 IDP
IDP_SAML2 = True
IDP_BACKENDS = ('authentic2.idp.saml.backend.SamlBackend',)
LOCAL_METADATA_CACHE_TIMEOUT = 600
SAML_METADATA_ROOT = 'metadata'
SAML_METADATA_AUTOLOAD = 'none'
AUTH_FRONTENDS = ( 'authentic2.auth2_auth.backend.LoginPasswordBackend',)
AUTHENTICATION_BACKENDS = (
'idp.auth_backend.PfBackend',
)
# pfidp use a specific auth backend, with a transient user
# authentic2 can not (yet?) handle this case without :
IDP_SAML2_AUTHN_CONTEXT_FROM_SESSION = False
# Default profile class
AUTH_PROFILE_MODULE = 'idp.UserProfile'
# now get some values from config.xml
# => server must be restarted if config.xml is changed
if 'CONFIG_XML' in os.environ:
# for run this application outside a real pfSense
CONFIG_XML = os.environ['CONFIG_XML']
else:
CONFIG_XML = '/cf/conf/config.xml'
idp = pfconfigxml.get_idp()
# SAML certificate
if idp:
SAML_SIGNATURE_PUBLIC_KEY = idp.get('saml_cert', {}).get('crt')
SAML_SIGNATURE_PRIVATE_KEY = idp.get('saml_cert', {}).get('prv')
#
# univnautes parameters
#
# store generated password here
CLEAR_PASSWORD_DIR = '/var/db/univnautes/pfidp/passwords'
# default expiration
IDP_UA_DEFAULT_EXPIRES = 7
#####
# useless but needed parameters...
ACCOUNT_ACTIVATION_DAYS = 2
EMAIL_HOST = 'localhost'
DEFAULT_FROM_EMAIL = 'webmaster@localhost'
# SSL Authentication
AUTH_SSL = False
SSLAUTH_CREATE_USER = False
# SAML2 Authentication
AUTH_SAML2 = False
# OpenID Authentication
AUTH_OPENID = False
# OATH Authentication
AUTH_OATH = False
# OpenID settings
IDP_OPENID = False
# CAS settings
IDP_CAS = False

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,182 @@
/*!
* Datepicker for Bootstrap
*
* Copyright 2012 Stefan Petre
* Licensed under the Apache License v2.0
* http://www.apache.org/licenses/LICENSE-2.0
*
*/
.datepicker {
top: 0;
left: 0;
padding: 4px;
margin-top: 1px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
/*.dow {
border-top: 1px solid #ddd !important;
}*/
}
.datepicker:before {
content: '';
display: inline-block;
border-left: 7px solid transparent;
border-right: 7px solid transparent;
border-bottom: 7px solid #ccc;
border-bottom-color: rgba(0, 0, 0, 0.2);
position: absolute;
top: -7px;
left: 6px;
}
.datepicker:after {
content: '';
display: inline-block;
border-left: 6px solid transparent;
border-right: 6px solid transparent;
border-bottom: 6px solid #ffffff;
position: absolute;
top: -6px;
left: 7px;
}
.datepicker > div {
display: none;
}
.datepicker table {
width: 100%;
margin: 0;
}
.datepicker td,
.datepicker th {
text-align: center;
width: 20px;
height: 20px;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
}
.datepicker td.day:hover {
background: #eeeeee;
cursor: pointer;
}
.datepicker td.day.disabled {
color: #eeeeee;
}
.datepicker td.old,
.datepicker td.new {
color: #999999;
}
.datepicker td.active,
.datepicker td.active:hover {
color: #ffffff;
background-color: #006dcc;
background-image: -moz-linear-gradient(top, #0088cc, #0044cc);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc));
background-image: -webkit-linear-gradient(top, #0088cc, #0044cc);
background-image: -o-linear-gradient(top, #0088cc, #0044cc);
background-image: linear-gradient(to bottom, #0088cc, #0044cc);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0044cc', GradientType=0);
border-color: #0044cc #0044cc #002a80;
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #0044cc;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
color: #fff;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.datepicker td.active:hover,
.datepicker td.active:hover:hover,
.datepicker td.active:focus,
.datepicker td.active:hover:focus,
.datepicker td.active:active,
.datepicker td.active:hover:active,
.datepicker td.active.active,
.datepicker td.active:hover.active,
.datepicker td.active.disabled,
.datepicker td.active:hover.disabled,
.datepicker td.active[disabled],
.datepicker td.active:hover[disabled] {
color: #ffffff;
background-color: #0044cc;
*background-color: #003bb3;
}
.datepicker td.active:active,
.datepicker td.active:hover:active,
.datepicker td.active.active,
.datepicker td.active:hover.active {
background-color: #003399 \9;
}
.datepicker td span {
display: block;
width: 47px;
height: 54px;
line-height: 54px;
float: left;
margin: 2px;
cursor: pointer;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
}
.datepicker td span:hover {
background: #eeeeee;
}
.datepicker td span.active {
color: #ffffff;
background-color: #006dcc;
background-image: -moz-linear-gradient(top, #0088cc, #0044cc);
background-image: -webkit-gradient(linear, 0 0, 0 100%, from(#0088cc), to(#0044cc));
background-image: -webkit-linear-gradient(top, #0088cc, #0044cc);
background-image: -o-linear-gradient(top, #0088cc, #0044cc);
background-image: linear-gradient(to bottom, #0088cc, #0044cc);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc', endColorstr='#ff0044cc', GradientType=0);
border-color: #0044cc #0044cc #002a80;
border-color: rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.1) rgba(0, 0, 0, 0.25);
*background-color: #0044cc;
/* Darken IE7 buttons by default so they stand out more given they won't have borders */
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
color: #fff;
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.datepicker td span.active:hover,
.datepicker td span.active:focus,
.datepicker td span.active:active,
.datepicker td span.active.active,
.datepicker td span.active.disabled,
.datepicker td span.active[disabled] {
color: #ffffff;
background-color: #0044cc;
*background-color: #003bb3;
}
.datepicker td span.active:active,
.datepicker td span.active.active {
background-color: #003399 \9;
}
.datepicker td span.old {
color: #999999;
}
.datepicker th.switch {
width: 145px;
}
.datepicker th.next,
.datepicker th.prev {
font-size: 21px;
}
.datepicker thead tr:first-child th {
cursor: pointer;
}
.datepicker thead tr:first-child th:hover {
background: #eeeeee;
}
.input-append.date .add-on i,
.input-prepend.date .add-on i {
display: block;
cursor: pointer;
width: 16px;
height: 16px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,474 @@
/* =========================================================
* bootstrap-datepicker.js
* http://www.eyecon.ro/bootstrap-datepicker
* =========================================================
* Copyright 2012 Stefan Petre
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ========================================================= */
!function( $ ) {
// Picker object
var Datepicker = function(element, options){
this.element = $(element);
this.format = DPGlobal.parseFormat(options.format||this.element.data('date-format')||'mm/dd/yyyy');
this.picker = $(DPGlobal.template)
.appendTo('body')
.on({
click: $.proxy(this.click, this)//,
//mousedown: $.proxy(this.mousedown, this)
});
this.isInput = this.element.is('input');
this.component = this.element.is('.date') ? this.element.find('.add-on') : false;
if (this.isInput) {
this.element.on({
focus: $.proxy(this.show, this),
//blur: $.proxy(this.hide, this),
keyup: $.proxy(this.update, this)
});
} else {
if (this.component){
this.component.on('click', $.proxy(this.show, this));
} else {
this.element.on('click', $.proxy(this.show, this));
}
}
this.minViewMode = options.minViewMode||this.element.data('date-minviewmode')||0;
if (typeof this.minViewMode === 'string') {
switch (this.minViewMode) {
case 'months':
this.minViewMode = 1;
break;
case 'years':
this.minViewMode = 2;
break;
default:
this.minViewMode = 0;
break;
}
}
this.viewMode = options.viewMode||this.element.data('date-viewmode')||0;
if (typeof this.viewMode === 'string') {
switch (this.viewMode) {
case 'months':
this.viewMode = 1;
break;
case 'years':
this.viewMode = 2;
break;
default:
this.viewMode = 0;
break;
}
}
this.startViewMode = this.viewMode;
this.weekStart = options.weekStart||this.element.data('date-weekstart')||0;
this.weekEnd = this.weekStart === 0 ? 6 : this.weekStart - 1;
this.onRender = options.onRender;
this.fillDow();
this.fillMonths();
this.update();
this.showMode();
};
Datepicker.prototype = {
constructor: Datepicker,
show: function(e) {
this.picker.show();
this.height = this.component ? this.component.outerHeight() : this.element.outerHeight();
this.place();
$(window).on('resize', $.proxy(this.place, this));
if (e ) {
e.stopPropagation();
e.preventDefault();
}
if (!this.isInput) {
}
var that = this;
$(document).on('mousedown', function(ev){
if ($(ev.target).closest('.datepicker').length == 0) {
that.hide();
}
});
this.element.trigger({
type: 'show',
date: this.date
});
},
hide: function(){
this.picker.hide();
$(window).off('resize', this.place);
this.viewMode = this.startViewMode;
this.showMode();
if (!this.isInput) {
$(document).off('mousedown', this.hide);
}
//this.set();
this.element.trigger({
type: 'hide',
date: this.date
});
},
set: function() {
var formated = DPGlobal.formatDate(this.date, this.format);
if (!this.isInput) {
if (this.component){
this.element.find('input').prop('value', formated);
}
this.element.data('date', formated);
} else {
this.element.prop('value', formated);
}
},
setValue: function(newDate) {
if (typeof newDate === 'string') {
this.date = DPGlobal.parseDate(newDate, this.format);
} else {
this.date = new Date(newDate);
}
this.set();
this.viewDate = new Date(this.date.getFullYear(), this.date.getMonth(), 1, 0, 0, 0, 0);
this.fill();
},
place: function(){
var offset = this.component ? this.component.offset() : this.element.offset();
this.picker.css({
top: offset.top + this.height,
left: offset.left
});
},
update: function(newDate){
this.date = DPGlobal.parseDate(
typeof newDate === 'string' ? newDate : (this.isInput ? this.element.prop('value') : this.element.data('date')),
this.format
);
this.viewDate = new Date(this.date.getFullYear(), this.date.getMonth(), 1, 0, 0, 0, 0);
this.fill();
},
fillDow: function(){
var dowCnt = this.weekStart;
var html = '<tr>';
while (dowCnt < this.weekStart + 7) {
html += '<th class="dow">'+DPGlobal.dates.daysMin[(dowCnt++)%7]+'</th>';
}
html += '</tr>';
this.picker.find('.datepicker-days thead').append(html);
},
fillMonths: function(){
var html = '';
var i = 0
while (i < 12) {
html += '<span class="month">'+DPGlobal.dates.monthsShort[i++]+'</span>';
}
this.picker.find('.datepicker-months td').append(html);
},
fill: function() {
var d = new Date(this.viewDate),
year = d.getFullYear(),
month = d.getMonth(),
currentDate = this.date.valueOf();
this.picker.find('.datepicker-days th:eq(1)')
.text(DPGlobal.dates.months[month]+' '+year);
var prevMonth = new Date(year, month-1, 28,0,0,0,0),
day = DPGlobal.getDaysInMonth(prevMonth.getFullYear(), prevMonth.getMonth());
prevMonth.setDate(day);
prevMonth.setDate(day - (prevMonth.getDay() - this.weekStart + 7)%7);
var nextMonth = new Date(prevMonth);
nextMonth.setDate(nextMonth.getDate() + 42);
nextMonth = nextMonth.valueOf();
var html = [];
var clsName,
prevY,
prevM;
while(prevMonth.valueOf() < nextMonth) {
if (prevMonth.getDay() === this.weekStart) {
html.push('<tr>');
}
clsName = this.onRender(prevMonth);
prevY = prevMonth.getFullYear();
prevM = prevMonth.getMonth();
if ((prevM < month && prevY === year) || prevY < year) {
clsName += ' old';
} else if ((prevM > month && prevY === year) || prevY > year) {
clsName += ' new';
}
if (prevMonth.valueOf() === currentDate) {
clsName += ' active';
}
html.push('<td class="day '+clsName+'">'+prevMonth.getDate() + '</td>');
if (prevMonth.getDay() === this.weekEnd) {
html.push('</tr>');
}
prevMonth.setDate(prevMonth.getDate()+1);
}
this.picker.find('.datepicker-days tbody').empty().append(html.join(''));
var currentYear = this.date.getFullYear();
var months = this.picker.find('.datepicker-months')
.find('th:eq(1)')
.text(year)
.end()
.find('span').removeClass('active');
if (currentYear === year) {
months.eq(this.date.getMonth()).addClass('active');
}
html = '';
year = parseInt(year/10, 10) * 10;
var yearCont = this.picker.find('.datepicker-years')
.find('th:eq(1)')
.text(year + '-' + (year + 9))
.end()
.find('td');
year -= 1;
for (var i = -1; i < 11; i++) {
html += '<span class="year'+(i === -1 || i === 10 ? ' old' : '')+(currentYear === year ? ' active' : '')+'">'+year+'</span>';
year += 1;
}
yearCont.html(html);
},
click: function(e) {
e.stopPropagation();
e.preventDefault();
var target = $(e.target).closest('span, td, th');
if (target.length === 1) {
switch(target[0].nodeName.toLowerCase()) {
case 'th':
switch(target[0].className) {
case 'switch':
this.showMode(1);
break;
case 'prev':
case 'next':
this.viewDate['set'+DPGlobal.modes[this.viewMode].navFnc].call(
this.viewDate,
this.viewDate['get'+DPGlobal.modes[this.viewMode].navFnc].call(this.viewDate) +
DPGlobal.modes[this.viewMode].navStep * (target[0].className === 'prev' ? -1 : 1)
);
this.fill();
this.set();
break;
}
break;
case 'span':
if (target.is('.month')) {
var month = target.parent().find('span').index(target);
this.viewDate.setMonth(month);
} else {
var year = parseInt(target.text(), 10)||0;
this.viewDate.setFullYear(year);
}
if (this.viewMode !== 0) {
this.date = new Date(this.viewDate);
this.element.trigger({
type: 'changeDate',
date: this.date,
viewMode: DPGlobal.modes[this.viewMode].clsName
});
}
this.showMode(-1);
this.fill();
this.set();
break;
case 'td':
if (target.is('.day') && !target.is('.disabled')){
var day = parseInt(target.text(), 10)||1;
var month = this.viewDate.getMonth();
if (target.is('.old')) {
month -= 1;
} else if (target.is('.new')) {
month += 1;
}
var year = this.viewDate.getFullYear();
this.date = new Date(year, month, day,0,0,0,0);
this.viewDate = new Date(year, month, Math.min(28, day),0,0,0,0);
this.fill();
this.set();
this.element.trigger({
type: 'changeDate',
date: this.date,
viewMode: DPGlobal.modes[this.viewMode].clsName
});
}
break;
}
}
},
mousedown: function(e){
e.stopPropagation();
e.preventDefault();
},
showMode: function(dir) {
if (dir) {
this.viewMode = Math.max(this.minViewMode, Math.min(2, this.viewMode + dir));
}
this.picker.find('>div').hide().filter('.datepicker-'+DPGlobal.modes[this.viewMode].clsName).show();
}
};
$.fn.datepicker = function ( option, val ) {
return this.each(function () {
var $this = $(this),
data = $this.data('datepicker'),
options = typeof option === 'object' && option;
if (!data) {
$this.data('datepicker', (data = new Datepicker(this, $.extend({}, $.fn.datepicker.defaults,options))));
}
if (typeof option === 'string') data[option](val);
});
};
$.fn.datepicker.defaults = {
onRender: function(date) {
return '';
}
};
$.fn.datepicker.Constructor = Datepicker;
var DPGlobal = {
modes: [
{
clsName: 'days',
navFnc: 'Month',
navStep: 1
},
{
clsName: 'months',
navFnc: 'FullYear',
navStep: 1
},
{
clsName: 'years',
navFnc: 'FullYear',
navStep: 10
}],
dates:{
days: ["Dimanche", "Lundi", "Mardi", "Mercredi", "Jeudi", "Vendredi", "Samedi", "Dimanche"],
daysShort: ["Dim", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dim"],
daysMin: ["Di", "Lu", "Ma", "Me", "Je", "Ve", "Sa", "Di"],
months: ["Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre"],
monthsShort: ["Jan", "Fév", "Mar", "Avr", "Mai", "Jui", "Jul", "Aoû", "Sep", "Oct", "Nov", "Déc"]
},
isLeapYear: function (year) {
return (((year % 4 === 0) && (year % 100 !== 0)) || (year % 400 === 0))
},
getDaysInMonth: function (year, month) {
return [31, (DPGlobal.isLeapYear(year) ? 29 : 28), 31, 30, 31, 30, 31, 31, 30, 31, 30, 31][month]
},
parseFormat: function(format){
var separator = format.match(/[.\/\-\s].*?/),
parts = format.split(/\W+/);
if (!separator || !parts || parts.length === 0){
throw new Error("Invalid date format.");
}
return {separator: separator, parts: parts};
},
parseDate: function(date, format) {
var parts = date.split(format.separator),
date = new Date(),
val;
date.setHours(0);
date.setMinutes(0);
date.setSeconds(0);
date.setMilliseconds(0);
if (parts.length === format.parts.length) {
var year = date.getFullYear(), day = date.getDate(), month = date.getMonth();
for (var i=0, cnt = format.parts.length; i < cnt; i++) {
val = parseInt(parts[i], 10)||1;
switch(format.parts[i]) {
case 'dd':
case 'd':
day = val;
date.setDate(val);
break;
case 'mm':
case 'm':
month = val - 1;
date.setMonth(val - 1);
break;
case 'yy':
year = 2000 + val;
date.setFullYear(2000 + val);
break;
case 'yyyy':
year = val;
date.setFullYear(val);
break;
}
}
date = new Date(year, month, day, 0 ,0 ,0);
}
return date;
},
formatDate: function(date, format){
var val = {
d: date.getDate(),
m: date.getMonth() + 1,
yy: date.getFullYear().toString().substring(2),
yyyy: date.getFullYear()
};
val.dd = (val.d < 10 ? '0' : '') + val.d;
val.mm = (val.m < 10 ? '0' : '') + val.m;
var date = [];
for (var i=0, cnt = format.parts.length; i < cnt; i++) {
date.push(val[format.parts[i]]);
}
return date.join(format.separator);
},
headTemplate: '<thead>'+
'<tr>'+
'<th class="prev">&lsaquo;</th>'+
'<th colspan="5" class="switch"></th>'+
'<th class="next">&rsaquo;</th>'+
'</tr>'+
'</thead>',
contTemplate: '<tbody><tr><td colspan="7"></td></tr></tbody>'
};
DPGlobal.template = '<div class="datepicker dropdown-menu">'+
'<div class="datepicker-days">'+
'<table class=" table-condensed">'+
DPGlobal.headTemplate+
'<tbody></tbody>'+
'</table>'+
'</div>'+
'<div class="datepicker-months">'+
'<table class="table-condensed">'+
DPGlobal.headTemplate+
DPGlobal.contTemplate+
'</table>'+
'</div>'+
'<div class="datepicker-years">'+
'<table class="table-condensed">'+
DPGlobal.headTemplate+
DPGlobal.contTemplate+
'</table>'+
'</div>'+
'</div>';
}( window.jQuery );

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,152 @@
// Stupid jQuery table plugin.
// Call on a table
// sortFns: Sort functions for your datatypes.
(function($) {
$.fn.stupidtable = function(sortFns) {
return this.each(function() {
var $table = $(this);
sortFns = sortFns || {};
// ==================================================== //
// Utility functions //
// ==================================================== //
// Merge sort functions with some default sort functions.
sortFns = $.extend({}, $.fn.stupidtable.default_sort_fns, sortFns);
// Return the resulting indexes of a sort so we can apply
// this result elsewhere. This returns an array of index numbers.
// return[0] = x means "arr's 0th element is now at x"
var sort_map = function(arr, sort_function) {
var map = [];
var index = 0;
var sorted = arr.slice(0).sort(sort_function);
for (var i=0; i<arr.length; i++) {
index = $.inArray(arr[i], sorted);
// If this index is already in the map, look for the next index.
// This handles the case of duplicate entries.
while ($.inArray(index, map) != -1) {
index++;
}
map.push(index);
}
return map;
};
// Apply a sort map to the array.
var apply_sort_map = function(arr, map) {
var clone = arr.slice(0),
newIndex = 0;
for (var i=0; i<map.length; i++) {
newIndex = map[i];
clone[newIndex] = arr[i];
}
return clone;
};
// ==================================================== //
// Begin execution! //
// ==================================================== //
// Do sorting when THs are clicked
$table.on("click", "th", function() {
var trs = $table.children("tbody").children("tr");
var $this = $(this);
var th_index = 0;
var dir = $.fn.stupidtable.dir;
$table.find("th").slice(0, $this.index()).each(function() {
var cols = $(this).attr("colspan") || 1;
th_index += parseInt(cols,10);
});
// Determine (and/or reverse) sorting direction, default `asc`
var sort_dir = $this.data("sort-default") || dir.ASC;
if ($this.data("sort-dir"))
sort_dir = $this.data("sort-dir") === dir.ASC ? dir.DESC : dir.ASC;
// Choose appropriate sorting function.
var type = $this.data("sort") || null;
// Prevent sorting if no type defined
if (type === null) {
return;
}
// Trigger `beforetablesort` event that calling scripts can hook into;
// pass parameters for sorted column index and sorting direction
$table.trigger("beforetablesort", {column: th_index, direction: sort_dir});
// More reliable method of forcing a redraw
$table.css("display");
// Run sorting asynchronously on a timout to force browser redraw after
// `beforetablesort` callback. Also avoids locking up the browser too much.
setTimeout(function() {
// Gather the elements for this column
var column = [];
var sortMethod = sortFns[type];
// Push either the value of the `data-order-by` attribute if specified
// or just the text() value in this column to column[] for comparison.
trs.each(function(index,tr) {
var $e = $(tr).children().eq(th_index);
var sort_val = $e.data("sort-value");
var order_by = typeof(sort_val) !== "undefined" ? sort_val : $e.text();
column.push(order_by);
});
// Create the sort map. This column having a sort-dir implies it was
// the last column sorted. As long as no data-sort-desc is specified,
// we're free to just reverse the column.
var theMap;
if (sort_dir == dir.ASC)
theMap = sort_map(column, sortMethod);
else
theMap = sort_map(column, function(a, b) { return -sortMethod(a, b); });
// Reset siblings
$table.find("th").data("sort-dir", null).removeClass("sorting-desc sorting-asc");
$this.data("sort-dir", sort_dir).addClass("sorting-"+sort_dir);
var sortedTRs = $(apply_sort_map(trs, theMap));
$table.children("tbody").remove();
$table.append("<tbody />").append(sortedTRs);
// Trigger `aftertablesort` event. Similar to `beforetablesort`
$table.trigger("aftertablesort", {column: th_index, direction: sort_dir});
// More reliable method of forcing a redraw
$table.css("display");
}, 10);
});
});
};
// Enum containing sorting directions
$.fn.stupidtable.dir = {ASC: "asc", DESC: "desc"};
$.fn.stupidtable.default_sort_fns = {
"int": function(a, b) {
return parseInt(a, 10) - parseInt(b, 10);
},
"float": function(a, b) {
return parseFloat(a) - parseFloat(b);
},
"string": function(a, b) {
if (a < b) return -1;
if (a > b) return +1;
return 0;
},
"string-ins": function(a, b) {
a = a.toLowerCase();
b = b.toLowerCase();
if (a < b) return -1;
if (a > b) return +1;
return 0;
}
};
})(jQuery);

View File

@ -0,0 +1,10 @@
import os
from django.conf import settings
from django.template.loaders.filesystem import Loader as FileSystemLoader
class Loader(FileSystemLoader):
def get_template_sources(self, template_name, template_dirs=None):
filename = os.path.join(settings.CPELEMENTS,
'captiveportal-idp-template-%s' % template_name.replace('/', '-'))
return (filename,)

View File

@ -0,0 +1,12 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %} {% trans "Error: page not found" %} {% endblock %}
{% block content %}
<h2>{% trans "Error: page not found" %}</h2>
<p>{% trans "The page you requested has not been found on this server." %}
{% endblock %}

View File

@ -0,0 +1,12 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %} {% trans "Error: server error (500)" %} {% endblock %}
{% block content %}
<h2>{% trans "Server Error" %}</h2>
<p>{% trans "We're sorry but a server error has occurred." %}
{% endblock %}

View File

@ -0,0 +1,10 @@
{% extends "admin/base.html" %}
{% load i18n %}
{% block title %}{{ title }} | {% trans 'Authentic site admin' %}{% endblock %}
{% block branding %}
<h1 id="site-name">{% trans 'Authentic administration' %}</h1>
{% endblock %}
{% block nav-global %}{% endblock %}

View File

@ -0,0 +1,45 @@
{% extends "admin/base_site.html" %}
{% load i18n %}
{% block title %} {% trans "Logs" %} {% endblock %}
{% block nav-global %}{% endblock %}
{% block breadcrumbs %}<div class="breadcrumbs"><a href="/admin/">{% trans "Home" %}</a> > Admin_log_view</div>{% endblock %}
{% block content %}
<h2>{% trans "Logs page" %}</h2>
{% if not file %}
<p> There is no log for the moment.</p>
{% else %}
{% for log in logs.object_list %}
{% if "CRITICAL" in log %}
<p> <strong> {{ log }} </strong> </p>
{% else %}
{% if "ERROR" in log %}
<p> <strong> {{ log }} </strong> </p>
{% else %}
<p> {{ log }} </p>
{% endif %}
{% endif %}
{% endfor %}
<div class = "pagination">
<span class = "step-links">
{% if logs.has_previous %}
<a href = "?page={{ logs.previous_page_number }}">previous</a>
{% endif %}
<span class="current">
Page {{ logs.number }} of {{ logs.paginator.num_pages }}.
</span>
{% if logs.has_next %}
<a href="?page={{ logs.next_page_number }}">next</a>
{% endif %}
</span>
</div>
{% endif %}
{% endblock %}

View File

@ -0,0 +1,22 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}
{% trans "Log in" %}
{% endblock %}
{% block content %}
{% if messages %}
<ul class="messages">
{% for message in messages %}
<li{% if message.tags %} class="{{ message.tags }}"{% endif %}>{{ message }}</li>
{% endfor %}
</ul>
{% endif %}
{% for name, content in methods %}
{{ content|safe }}
{% endfor %}
{% endblock %}

View File

@ -0,0 +1,32 @@
{% load i18n %}
<form method="post" action="" class="form-horizontal">
{% csrf_token %}
{% if form.non_field_errors %}
{% for error in form.non_field_errors %}
<p><strong>{{ error }}</strong></p>
{% endfor %}
{% endif %}
<div class="control-group">
<label class="control-label" for="id_username">{% trans "Username" %}
{% for error in form.username.errors %}<br /><i class="icon-warning-sign"></i> <font color="red">{{ error|escape }}</font>{% endfor %}
</label>
<div class="controls">
{{ form.username }}
</div>
</div>
<div class="control-group">
<label class="control-label" for="id_password">{% trans "Password" %}
{% for error in form.password.errors %}<br /><i class="icon-warning-sign"></i> <font color="red">{{ error|escape }}</font>{% endfor %}
</label>
<div class="controls">
{{ form.password }}
</div>
</div>
<div class="control-group"><div class="controls">
<input type="submit" name="{{ submit_name }}" value="{% trans "Log in" %}" class="btn"/>
</div></div>
</form>

View File

@ -0,0 +1,85 @@
{% load i18n %}{% load staticfiles %}<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>eduspot :: {% block title %}{% endblock %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="eduspot / IdP local">
<meta name="author" content="UNPIdF - Entr'ouvert www.entrouvert.com">
<!-- Le styles -->
<link href="{% static "bootstrap/css/bootstrap.css" %}" rel="stylesheet">
<style type="text/css">
body { padding-top: 60px; padding-bottom: 40px; }
.sidebar-nav { padding: 9px 0; }
</style>
<link href="{% static "bootstrap/css/bootstrap-responsive.css" %}" rel="stylesheet">
<!-- HTML5 shim, for IE6-8 support of HTML5 elements -->
<!--[if lt IE 9]>
<script src="{% static "html5shiv/js/html5shiv.js" %}"></script>
<![endif]-->
{% block head %}{% endblock %}
<!-- Fav and touch icons -->
<link rel="shortcut icon" href="{% static "univnautes/img/favicon.ico" %}">
<!-- local CSS -->
<link rel="stylesheet" href="{% static "univnautes/css/univnautes.css" %}"/>
<!--[if lte IE 8]><link rel="stylesheet" href="{% static "univnautes/css/univnautes.ie.css" %}"/><![endif]-->
</head>
<body {% block body_args %}{% endblock %} {% block bodyargs %}{% endblock %}>
<div class="navbar navbar-inverse navbar-fixed-top">
<div class="navbar-inner">
<div class="container-fluid">
<a class="btn btn-navbar" data-toggle="collapse" data-target=".nav-collapse">
<span class="icon-bar"></span>
<span class="icon-bar"></span>
<span class="icon-bar"></span>
</a>
<a class="brand" href="/"><i class="icon-eduspot"></i> Accès eduspot</a>
<div class="nav-collapse collapse">
<ul class="nav">
<!-- li><a href="/conditions">Conditions d'utilisation</a></li -->
{% if mailform %}<li><a href="/mail">Contact <i class="icon-envelope icon-white"></i></a></li>{% endif %}
</ul>
</div><!--/.nav-collapse -->
</div>
</div>
</div>
<div class="container-fluid">
<div class="row-fluid">
{% block header %}{% endblock %}
</div>
<div class="row-fluid">
{% block disclaimer %}{% endblock %}
</div>
{% block content %}{% endblock %}
<hr>
<footer>
<p>&copy; UNPIdF 2014
{% block footer %}{% endblock %}
</footer>
</div><!--/.fluid-container-->
<!-- Le javascript
================================================== -->
<!-- Placed at the end of the document so the pages load faster -->
<script src="{% static "jquery/js/jquery-1.10.2.min.js" %}"></script>
<script src="{% static "bootstrap/js/bootstrap.min.js" %}"></script>
{% block end %}{% endblock %}
</body>
</html>

View File

@ -0,0 +1,9 @@
{% extends "base.html" %}
{% load i18n %}
{% block bodyargs %}onload="setTimeout(function () { window.location='{{ next_page }}' }, {{ redir_timeout }})"{% endblock %}
{% block content %}
<h1>{{ title }}</h1>
{{ msg }}
<p><a href="{{ back }}">{% trans "Back" %}<a/></p>
{% endblock %}

View File

@ -0,0 +1,13 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %} {% trans "Error: authentication failure" %} {% endblock %}
{% block content %}
<h2>{% trans "Authentication failure" %}</h2>
<p>{% trans "The SSL authentication has failed" %}</p>
<a href="/">Back</a>
{% endblock %}

View File

@ -0,0 +1,30 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}
{% trans "Authentic - Account Management" %}
{% endblock %}
{% block content %}
<h2>{% trans "Account Management" %}</h2>
<h3>{% trans "Profile" %}</h3>
<div id="profile">
{% if profile %}
<dl>
{% for key, value in profile %}
<dt>{{ key|capfirst }}</dt>
<dd>{{ value }}</dd>
{% endfor %}
</dl>
<p> <a href="{% url "profiles_edit_profile" %}">{% trans "Edit profile" %}</a></p>
{% else %}
<p> <a href="{% url "profiles_create_profile" %}">{% trans "Create profile" %}</a></p>
{% endif %}
</div>
<h3>{% trans "Credentials" %}</h3>
{% for html_block in frontends_block %}
{{ html_block|safe }}
{% endfor %}
<p><a href="/">{% trans "Back" %}<a/></p>
{% endblock %}

View File

@ -0,0 +1,20 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}
{% trans "Authentic2 connected" %}
{% endblock %}
{% block content %}
<h2>
Connect&eacute; en tant que &laquo;&nbsp;{{ user.username }}&nbsp;&raquo;
</h2>
<h3>{% if user.displayname %}{{ user.displayname }}{% endif %}</h3>
{% if user.is_staff %}
<a class="btn btn-success" id="users_admin" href="/users-admin/"><i class="icon-wrench"></i> Gestion des invit&eacute;s</a>
{% endif %}
<a class="btn" id="logout" href="{% url "auth_logout" %}"><i class="icon-off"></i> {% trans "Log out" %}</a>
{% endblock %}

View File

@ -0,0 +1,20 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %}
{% trans "Logout" %}
{% endblock %}
{% block bodyargs %}onload="setTimeout(function () { window.location='{{ next_page }}' }, {{ redir_timeout }})"{% endblock %}
{% block content %}
<h1>{% trans message %}</h1>
<ul class="logout-list">
{% for fragment in logout_list %}
{{ fragment|safe }}
{% endfor %}
</ul>
<div id="continue-link"><a href="{{ next_page }}">{% trans "Continuer" %}</div>
{% endblock %}

View File

@ -0,0 +1,17 @@
{% load i18n %}{% load staticfiles %}<!DOCTYPE html>
<html lang="fr">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<link rel="stylesheet" href="{% static "css/style.css" %}" />
<title>{% block title %}User test{% endblock %}</title>
{% block extra_scripts %}
{% endblock %}
</head>
<body id="iframe" {% block bodyargs %}{% endblock %} >
<div style="width: 400px; height: 200px; margin: auto">
{% block content %}{% endblock %}
</div>
</body>
</html>

View File

@ -0,0 +1,37 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %} {% trans "Consent page" %} {% endblock %}
{% block content %}
{% load i18n %}
<div id="consent">
<p>
{% if provider_id %}
{% trans "Do you accept to federate your account with " %} <strong>{{ provider_id }}</strong> ?
{% else %}
{% trans "Do you accept to federate your account ?" %}
{% endif %}
{{ provider_id2 }}
</p>
<p>
{% if attributes %}
{% trans "Do you accept to send these attributes?" %}
<ul>
{% for attribute in attributes %}
<li>{{ attribute }}</li>
{% endfor %}
</ul>
{% endif %}
</p>
<form method="post" action="">
<input type="hidden" name="next" value="{{ next }}" />
<input type="hidden" name="nonce" value="{{ nonce }}" />
<input type="submit" name="accept" value="{% trans 'Accept' %}"/>
<input type="submit" name="refuse" value="{% trans 'Refuse' %}"/>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,33 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %} {% trans "Consent page for attribute propagation" %} {% endblock %}
{% block content %}
{% load i18n %}
<div id="consent">
<p>
{% if attributes %}
{% if provider_id %}
{% trans "Do you accept to send these attributes to " %} <strong>{{ provider_id }}</strong> ?
{% else %}
{% trans "Do you accept to send these attributes?" %}
{% endif %}
<ul>
{% for attribute in attributes %}
<li>{{ attribute }}</li>
{% endfor %}
</ul>
{% endif %}
</p>
<form method="post" action="">
<input type="hidden" name="next" value="{{ next }}" />
<input type="hidden" name="nonce" value="{{ nonce }}" />
<input type="submit" name="accept" value="{% trans 'Accept' %}"/>
<input type="submit" name="refuse" value="{% trans 'Refuse' %}"/>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,27 @@
{% extends "base.html" %}
{% load i18n %}
{% block title %} {% trans "Consent page for federation" %} {% endblock %}
{% block content %}
{% load i18n %}
<div id="consent">
<p>
{% if provider_id %}
{% trans "Do you accept to federate your account with " %} <strong>{{ provider_id }}</strong> ?
{% else %}
{% trans "Do you accept to federate your account ?" %}
{% endif %}
{{ provider_id2 }}
</p>
<form method="post" action="">
<input type="hidden" name="next" value="{{ next }}" />
<input type="hidden" name="nonce" value="{{ nonce }}" />
<input type="submit" name="accept" value="{% trans 'Accept' %}"/>
<input type="submit" name="refuse" value="{% trans 'Refuse' %}"/>
</form>
</div>
{% endblock %}

View File

@ -0,0 +1,12 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<!-- Close any popup enclosing us -->
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<title>Redirect to {{ next }}</title>
</head>
<body>
<script type="text/javascript">
window.open('{{ next }}', '_top');
</script>
</body>

View File

@ -0,0 +1,66 @@
{% load staticfiles %}<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>UnivNautes IdP / Gestion des utilisateurs / {% block title %}{% endblock %}</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="">
<link href="{% static "bootstrap/css/bootstrap.css" %}" rel="stylesheet">
<style>
body { padding-top: 60px; }
th { text-align: right; padding-right: 20px; vertical-align: top; }
.disabled { color: #ccc; }
ul.errorlist { list-style-type: none; margin: 0; }
ul.errorlist li { color: #f00; }
.helptext { font-size: 0.8em; font-style: italic; }
</style>
<link href="{% static "bootstrap/css/bootstrap-responsive.css" %}" rel="stylesheet">
<link href="{% static "bootstrap/css/datepicker.css" %}" rel="stylesheet">
<!-- HTML5 shim, for IE6-8 support of HTML5 elements -->
<!--[if lt IE 9]>
<script src="{% static "html5shiv/js/html5shiv.js" %}></script>
<![endif]-->
<script type="text/javascript" src="{% static "jquery/js/jquery-1.10.2.min.js" %}"></script>
<script type="text/javascript" src="{% static "bootstrap/js/bootstrap.min.js" %}"></script>
</head>
<body>
<div class="navbar navbar-fixed-top">
<div class="navbar-inner">
<div class="container">
<a class="brand" href="/users-admin/">UnivNautes IdP</a>
<div class="btn-group pull-right"><a href="/logout" class="btn"><i class="icon-off"></i> Déconnexion</a></div>
<div class="nav">
<ul class="nav">{% block nav %}{% endblock %}</ul>
</div>
</div>
</div>
</div>
<div class="container">
{% block title_content %}{% endblock %}
{% block messages %}
{% if messages %}
{% for message in messages %}
<div class="alert alert-block">
<a class="close" data-dismiss="alert" href="#">×</a>
{{ message }}
</div>
{% endfor %}
<script>$(".alert").alert();</script>
{% endif %}
{% endblock %}
{% block content %}{% endblock %}
</div>
</body>
</html>

View File

@ -0,0 +1,49 @@
{% extends "users_admin/base.html" %}
{% load i18n %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
<h1>{{ title }}</h1>
<br />
<p>
{{ text }}
</p>
{% if users %}
<table class="table table-condensed table-striped" id="usersTable">
<thead>
<tr>
<th class="type-string">login</th>
<th class="type-string">expiration</th>
<th class="type-string">nom complet</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr>
<td>
{{ user.name }}
{% if 'univnautes-idp-multiple' in user.priv %}(multiple){% endif %}
</td>
<td>{{ user.expires|default:"jamais" }} ({{ user.ttl }})</td>
<td>{{ user.descr }}</td>
</tr
{% endfor %}
</tbody>
</table>
{% endif %}
<form action="" method="post" autocomplete="off">
{% csrf_token %}
<div class="form-actions">
<input type="submit" value="Confirmer" class="btn btn-primary" />
<a href="/users-admin/" class="btn"><i class="icon-remove"></i> Annuler</a>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,30 @@
{% extends "users_admin/base.html" %}
{% load i18n %}
{% load staticfiles %}
{% block title %}Création d'utilisateur{% endblock %}
{% block content %}
<h1>Créer un utilisateur <em>login</em><h1>
<h3>ou un ensemble d'utilisateurs <em>login-N</em></h2>
<br />
<form action="" method="post" autocomplete="off">
{% csrf_token %}
<table>
{{ form.as_table }}
</table>
<div class="form-actions">
<input type="submit" value="Créer" class="btn btn-primary" />
<a href="." class="btn"><i class="icon-remove"></i> Annuler</a>
</div>
</form>
<script src="{% static "bootstrap/js/bootstrap-datepicker.js" %}"></script>
<script>
$(function(){ $('input.datepicker').datepicker({ format: 'dd/mm/yyyy', weekStart: 1 }); });
</script>
{% endblock %}

View File

@ -0,0 +1,46 @@
{% extends "users_admin/base.html" %}
{% load i18n %}
{% load staticfiles %}
{% block title %}Importer des utilisateus{% endblock %}
{% block content %}
<h1>Importer des utilisateurs</h1>
<br />
<form action="" method="post" enctype="multipart/form-data">
{% csrf_token %}
<table>
{{ form.as_table }}
</table>
<div class="row">
<div class="span12">
<h3>Format du fichier attendu : CSV</h3>
<ul>
<li>4 colonnes : login, nom complet, date d'expiration, mot de passe ;</li>
<li>date au format AAAA-MM-DD ;</li>
<li>si le mot de passe n'est pas fourni, un mot de passe sera généré ;</li>
<li>si l'utilisateur existe déjà, il sera mis à jour ;</li>
<li>séparateur : la virgule ;</li>
<li>codage UTF-8 ;</li>
<li>la première ligne ne sera pas prise en compte.</li>
</ul>
<h3>Exemple</h3>
<pre class="span6">
"login","nom complet","expiration","mot de passe"
"joe","Joseph","2016-01-01","GxLGFP2"
"bar","Anne Honyme","2010-01-01",""
"team","","2014-07-22",""
</pre>
</div></div>
<div class="form-actions">
<input type="submit" value="Importer" class="btn btn-primary" />
<a href="./" class="btn"><i class="icon-remove"></i> Annuler</a>
</div>
</form>
{% endblock %}

View File

@ -0,0 +1,108 @@
{% extends "users_admin/base.html" %}
{% load i18n %}
{% load staticfiles %}
{% block title %}Accueil{% endblock %}
{% block nav %}
<li><a href="create"><i class="icon-plus"></i> Créer des utilisateurs</a></li>
<li><a href="import"><i class="icon-download"></i> Importer des utilisateurs</a></li>
{% endblock %}
{% block title_content %}
<div class="row">
<div class="span8">
<h1>Liste des utilisateurs</h1>
</div>
<div class="span4">
<form method="get" action="" class="form-inline pull-right">
<div class="input-prepend input-append">
<span class="add-on"><a href="./?filter="><i class="icon-remove-circle"></i></a></span>
<input id="filter" type="text" name="filter" value="{{ filter }}" style="color: red; font-weight: bold;" placeholder="Filtre (avec * et ?)"/>
<input type="submit" value="Filtrer" class="btn" />
</div>
</form>
</div>
</div>
{% endblock %}
{% block content %}
<form action="multiple" method="post">
<br />
<table class="table table-condensed table-striped" id="usersTable">
<thead>
<tr>
<th></th>
<th data-sort="string">login</th>
<th data-sort="string">actif?</th>
<th data-sort="string">expiration</th>
<th data-sort="string">nom complet</th>
<th><input type="checkbox" id="check-all" /></th>
<script>
$('#check-all').click(function (event) {
$('input[name="users"]').each(function () {
this.click();
});
});
</script>
</tr>
</thead>
<tbody>
{% for user in users.values|dictsort:"name" %}
<tr {% if user.disabled or user.ttl == 0 %} class="disabled"{% endif %}>
<td style="text-align: right;">
<a href="update/{{ user.name }}"><i class="icon-edit"></i></a>
</td>
<td data-order-by="{{ user.name }}">
<a href="read/{{ user.name }}" {% if user.disabled or user.ttl == 0 %} class="disabled"{% endif %}>{{ user.name }}</a>
{% if 'univnautes-idp-multiple' in user.priv %} &mdash; multiple{% endif %}
{% if user.disabled %} &mdash; désactivé{% endif %}
{% if user.ttl == 0 %} &mdash; expiré{% endif %}
</td>
<td data-order-by="{% if user.disabled %}DIS{% elif user.ttl == 0 %}EXP{% else %}ACT{% endif %}">
{% if user.disabled %}<i class="icon-pause"></i>
{% elif user.ttl == 0 %}<i class="icon-stop"></i>
{% endif %}
</td>
<td data-order-by="{{ user.expires|date:"c"|default:"2999-12-31" }}">
{{ user.expires|default:"jamais" }}
{% if user.ttl > 0 %} ({{ user.ttl }} jour{% if user.ttl > 1 %}s{% endif %})
{% elif user.ttl == 0 %} &mdash; expiré{% endif %}
</td>
<td>{{ user.descr }}</td>
<td><input type="checkbox" name="users" value="{{ user.name }}" /></td>
</tr>
{% endfor %}
</tbody>
</table>
<div class="form-actions">
<a href="create" class="btn"><i class="icon-plus"></i> Créer des utilisateurs</a>
<a href="import" class="btn"><i class="icon-download"></i> Importer des utilisateurs</a>
<div class=" pull-right">
<input type="submit" value="Ok" class="btn pull-right" />
<select name="action" class="pull-right">
<option value="none" selected="selected">Choisir une action ...</option>
<option value="desactivate">Désactiver les utilisateurs choisis</option>
<option value="activate">Activer les utilisateurs choisis</option>
<option value="delete">Supprimer les utilisateurs choisis</option>
<option value="read">Afficher les utilisateurs choisis</option>
<option value="csv">Exporter en CSV les utilisateurs choisis</option>
<!-- option value="expire">Changer la date d'expiration des utilisateurs choisis</option -->
</select>
</div>
</div>
{% csrf_token %}
</form>
<script src="{% static "jquery/js/stupidtable.js" %}"></script>
<script>
$(function(){ $("#usersTable").stupidtable(); });
</script>
{% endblock %}

View File

@ -0,0 +1,71 @@
{% load staticfiles %}<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="utf-8">
<title>Utilisateurs UnivNautes</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="">
<meta name="author" content="">
<link href="{% static "bootstrap/css/bootstrap.css" %}" rel="stylesheet">
<style>
body { padding-top: 60px; }
th { text-align: right; padding-right: 20px; vertical-align: top; }
.disabled { color: #ccc; }
ul.errorlist { list-style-type: none; margin: 0; }
ul.errorlist li { color: #f00; }
.helptext { font-size: 0.8em; font-style: italic; }
div.user-block {
position: relative;
margin: 15px 0;
padding: 0px 19px 14px;
background-color: #fff;
border: 2px solid #222;
-webkit-border-radius: 4px;
-moz-border-radius: 4px;
border-radius: 4px;
}
span.user-descr {
color: #777;
}
</style>
<link href="{% static "bootstrap/css/bootstrap-responsive.css" %}" rel="stylesheet">
<!-- HTML5 shim, for IE6-8 support of HTML5 elements -->
<!--[if lt IE 9]>
<script src="{% static "html5shiv/js/html5shiv.js" %}></script>
<![endif]-->
<script type="text/javascript" src="{% static "jquery/js/jquery-1.10.2.min.js" %}"></script>
<script type="text/javascript" src="{% static "bootstrap/js/bootstrap.min.js" %}"></script>
</head>
<body>
<div class="navbar navbar-fixed-top">
<div class="navbar-inner">
<div class="container">
<span class="brand">Utilisateurs IdP UnivNautes &mdash; {% now "j F Y H:i" %}</span>
</div>
</div>
</div>
<div class="container">
{% for user in users %}
<div class="user-block">
<h2>
Login : {{ user.name }}
{% if user.descr %}<span class="user-descr"> &mdash; {{ user.descr }}</span>{% endif %}
</h2>
{% if user.password %}<h4>Mot de passe : {{ user.password }}</h4>{% endif %}
<p>Expire le {{ user.expires }}</p>
</div>
{% endfor %}
</div>
</body>
</html>

View File

@ -0,0 +1,44 @@
{% extends "users_admin/base.html" %}
{% load i18n %}
{% load staticfiles %}
{% block title %}Utilisateur {{ user.name}}{% endblock %}
{% block content %}
<h1>Utilisateur &lt;{{ user.name }}&gt;</h1>
<span style="font-size: 1.2em;">
<dl class="dl-horizontal">
<dt>Login</dt> <dd>{{ user.name }}
{% if user.disabled %}
&mdash; <strong>désactivé</strong>
{% endif %}
</dd>
<dt>Nom complet</dt> <dd>{{ user.descr }}&nbsp;</dd>
<dt>Expiration</dt> <dd>{{ user.expires }} &mdash;
{% if user.ttl %}{{user.ttl}} jour{% if user.ttl > 1 %}s{% endif %}{% else %}<strong>expiré</strong>{% endif %}</dd>
{% if user.password %}
<dt>Mot de passe</dt> <dd><span id="hidepass" style="display: none;">{{ user.password }}</span>
<span id="showpass">(cliquer ici pour l'afficher)</span>
<script>
$('#showpass').click(function() {$('#hidepass').show(); $('#showpass').hide();});
$('#hidepass').click(function() {$('#showpass').show(); $('#hidepass').hide();});
</script>
</dd>
{% endif %}
</dl>
</span>
<div class="form-actions">
<a href=".." class="btn btn-primary"><i class="icon-list"></i> Retour à la liste</a>
<a href="../update/{{ user.name }}" class="btn"><i class="icon-edit"></i> Modifier l'utilisateur</a>
{% if user.disabled %}
<a href="../activate/{{ user.name }}" class="btn"><i class="icon-play"></i> Activer l'utilisateur</a>
{% else %}
<a href="../desactivate/{{ user.name }}" class="btn"><i class="icon-pause"></i> Désactiver l'utilisateur</a>
{% endif %}
<a href="../delete/{{ user.name }}" class="btn btn-danger pull-right"><i class="icon-trash"></i> Supprimer l'utilisateur</a>
</div>
{% endblock %}

View File

@ -0,0 +1,36 @@
{% extends "users_admin/base.html" %}
{% load i18n %}
{% load staticfiles %}
{% block title %}Modifier un utilisateur{% endblock %}
{% block content %}
<h1>Modifier l'utilisateur &lt;{{ user.name }}&gt;</h1>
<br />
<form action="" method="post" autocomplete="off">
{% csrf_token %}
<table>
{{ form.as_table }}
</table>
<div class="form-actions">
<input type="submit" value="Modifier l'utilisateur" class="btn btn-primary" />
{% if user.disabled %}
<a href="../activate/{{ user.name }}" class="btn"><i class="icon-play"></i> Activer l'utilisateur</a>
{% else %}
<a href="../desactivate/{{ user.name }}" class="btn"><i class="icon-pause"></i> Désactiver l'utilisateur</a>
{% endif %}
<a href=".." class="btn"><i class="icon-remove"></i> Annuler</a>
<a href="../delete/{{ user.name }}" class="btn btn-danger pull-right"><i class="icon-trash"></i> Supprimer l'utilisateur</a>
</div>
</form>
<script src="{% static "bootstrap/js/bootstrap-datepicker.js" %}"></script>
<script>
$(function(){ $('input.datepicker').datepicker({ format: 'dd/mm/yyyy', weekStart: 1 }); });
</script>
{% endblock %}

View File

@ -0,0 +1,2 @@
"login","nom complet","expiration","mot de passe"{% for user in users %}
"{{ user.name|addslashes }}","{{ user.descr|safe|addslashes }} ","{{ user.expires|date:"c" }}","{{ user.password|default_if_none:"-"|safe|addslashes }}"{% endfor %}
Can't render this file because it contains an unexpected character in line 1 and column 49.

View File

@ -0,0 +1,21 @@
from django.conf.urls.defaults import *
from django.contrib.auth.decorators import login_required
from django.conf import settings
import authentic2.idp.views
urlpatterns = patterns('',
url(r'^', include('authentic2.auth2_auth.urls')),
url(r'^logout$', 'authentic2.idp.views.logout', name='auth_logout'),
url(r'^idp/', include('authentic2.idp.urls')),
url(r'^users-admin/', include('idp.users_admin.urls')),
url(r'^static/(?P<path>.*)$', 'idp.views.static_serve'),
url(r'^$', login_required(authentic2.idp.views.homepage), {}, 'index'),
)
if settings.DEBUG:
from django.contrib import admin
admin.autodiscover()
urlpatterns += patterns('',
(r'^admin/', include(admin.site.urls)),
)

View File

@ -0,0 +1,75 @@
# -*- encoding: utf-8 -*-
from django import forms
from django.conf import settings
import datetime
import pfusers
class ConfirmForm(forms.Form):
pass
class UploadFileForm(forms.Form):
file = forms.FileField(label=u"Fichier", required=True)
class UserForm(forms.Form):
name = forms.RegexField(label=u"Nom d'utilisateur (login)", regex='^[a-z0-9\.\-_]+$', min_length=3, max_length=16, required=True,
widget=forms.TextInput(attrs={'readonly': True, 'size':'16', 'autocomplete':'off', 'class':'span2'}))
disabled = forms.BooleanField(label=u'Désactivé',required=False)
password = forms.CharField(label=u"Nouveau mot de passe", min_length=3, max_length=64, required=False,
widget=forms.PasswordInput(attrs={'size':'32', 'autocomplete':'off', 'class':'span3'}))
password2 = forms.CharField(label=u"Mot de passe (vérification)", min_length=3, max_length=64, required=False,
widget=forms.PasswordInput(attrs={'size':'32', 'autocomplete':'off', 'class':'span3'}))
expires = forms.DateField(label=u"Date d'expiration", required=False,
widget=forms.TextInput(attrs={'size':'16', 'class':'span2 datepicker'}))
descr = forms.CharField(label=u'Description (nom long)', max_length=256, required=False,
widget=forms.TextInput(attrs={'size':'256', 'class':'span6'}))
multiple = forms.BooleanField(label=u'Connexions multiples autorisées',required=False)
def is_valid(self):
valid = super(UserForm, self).is_valid()
if valid:
# check passwords
password = self.cleaned_data.get('password')
password2 = self.cleaned_data.get('password2')
if password != password2:
self.errors['password'] = [u'Les deux mots de passe doivent être identiques']
valid = False
return valid
class NewUserForm(UserForm):
name = forms.RegexField(label=u"Nom d'utilisateur (login)", regex='^[a-z0-9\.\-_]+$', min_length=3, max_length=16, required=True,
widget=forms.TextInput(attrs={'size':'16', 'autocomplete':'off', 'class':'span2'}),
help_text="""Uniquement lettres, chiffres, point, trait d'union et «_».
Si création de plusieurs utilisateurs (voir plus bas), leur login sera de la forme login-N.""")
# password and expire fields
password = forms.CharField(label=u"Mot de passe", min_length=3, max_length=64, required=False,
help_text="""Si vous n'indiquez pas de mot de passe, un mot de passe aléatoire sera
attribué à chaque utilisateur""",
widget=forms.PasswordInput(attrs={'size':'32', 'autocomplete':'off', 'class':'span3'}))
expires = forms.DateField(label=u"Date d'expiration", required=True,
widget=forms.TextInput(attrs={'size':'16', 'class':'span2 datepicker'}))
userset_number = forms.IntegerField(label=u"Nombre d'utilisateur(s) à créer (login-N)",
widget=forms.TextInput(attrs={'size':'5', 'class':'span1'}),
required=True, min_value=1)
userset_start = forms.IntegerField(label=u"""En cas de création de plusieurs utilisateurs (login-N),
indiquer le premier numéro N""",
widget=forms.TextInput(attrs={'size':'5', 'class':'span1'}),
required=True)
def is_valid(self):
valid = super(NewUserForm, self).is_valid()
if valid:
name = self.cleaned_data.get('name')
all_pfusers = pfusers.get_all_pfusers()
userset_number = int(self.cleaned_data.get('userset_number'))
if userset_number == 1 and all_pfusers.get(name, None) is not None:
self.errors['name'] = [u'Un utilisateur avec ce login existe déjà.']
return False
if userset_number > 1:
userset_start = int(self.cleaned_data.get('userset_start'))
for n in range(userset_start, userset_start + userset_number):
if all_pfusers.get('%s-%d' % (name, n), None) is not None:
self.errors['name'] = [u"L'utilisateur %s-%d existe déjà." % (name, n)]
return False
return valid

View File

@ -0,0 +1,227 @@
#!/usr/local/bin/php -f
<?php
/*
* pf_useradm [action] [arg0=...] [arg1=....] ...
* action: create | delete | modify
* args: name, password, descr, expires, disabled, groups (comma separated), privs (comma separated)
*
* Note : a lot of this code is borrowed from pfsense /usr/local/www/system_usermanager.php
*
*/
$actions = array("create", "update", "delete");
$keys = array("name", "password", "descr", "expires", "disabled", "groups", "privs");
array_shift($argv);
$action = $argv[0];
if (!in_array($action, $actions)) {
echo "BAD SYNTAX: incorrect action '" . $action . "'\n";
exit(2);
}
array_shift($argv);
$userargs=array();
foreach ($argv as $arg) {
list($name, $value) = explode('=', $arg, 2);
if (in_array($name, $keys)) {
$userargs[$name] = $value;
} else {
echo "BAD SYNTAX: incorrect arg '" . $name . "'\n";
exit(2);
}
}
// check and normalize
if (!$userargs['name']) {
echo "ERROR: missing name\n";
exit(2);
}
if (preg_match("/[^a-zA-Z0-9\.\-_]/", $userargs['name'])) {
echo "ERROR: invalid characters in name\n";
exit(2);
}
if (strlen($userargs['name']) > 16) {
echo "ERROR: too long name (16 chars max)\n";
exit(2);
}
if ($userargs['expires']) {
$expires = strtotime($userargs['expires']);
if ($expires > 0) {
$userargs['expires'] = date("m/d/Y",$expires);
} else {
echo "CANNOT CREATE: bad expiration '" . $userargs['expires'] . "'date (use mm/dd/yyyy)\n";
exit(3);
}
} else
unset($userargs['expires']);
if (preg_match("/^[Yy]/i", $userargs['disabled']))
$userargs['disabled']= true;
else
unset($userargs['disabled']);
if ($userargs['privs'])
$userargs['priv'] = explode(',', $userargs['privs']);
elseif (isset($userargs['privs'])) // handle privs=""
$userargs['priv'] = array();
unset($userargs['privs']);
if ($userargs['groups'])
$groups = explode(',', $userargs['groups']);
elseif (isset($userargs['groups'])) // handle groups=""
$groups = array();
else
unset($groups);
unset($userargs['groups']);
// now we need some functions and globals variables ($config)
require("auth.inc");
// check that groups exist
if (isset($groups)) {
$conf_groups = array();
foreach ($config['system']['group'] as $gidx => $group) {
$conf_groups[] = $group['name'];
}
foreach ($groups as $group)
if (!in_array($group, $conf_groups)) {
echo "ERROR: group '" . $group . "' does not exist\n";
exit(2);
}
}
// search the user...
$user = getUserEntry($userargs["name"]);
// handle the action
if ($action == "create") {
if ($user) {
echo "CANNOT CREATE: user '" . $userargs['name'] . "' already exists\n";
exit(3);
}
// encrypt password
if ($userargs['password'])
local_user_set_password($userargs, $userargs['password']);
else {
echo "CANNOT CREATE: missing password\n";
exit(3);
}
// add last bits in the user
$userargs['scope'] = 'user';
$userargs['authorizedkeys'] = '';
$userargs['ipsecpsk'] = '';
$system_users = explode("\n", file_get_contents("/etc/passwd"));
foreach ($system_users as $s_user) {
$ent = explode(":", $s_user);
if ($ent[0] == $userargs['name']) {
echo "ERROR: name '" . $userargs['name'] . "' is reserved by the system\n";
exit(2);
}
}
conf_mount_rw();
$new_user = $userargs;
$new_user['uid'] = $config['system']['nextuid']++;
// Add the user to "All Users" group (borrowed from pfsense, don't known if its usefull)
foreach ($config['system']['group'] as $gidx => $group) {
if ($group['name'] == "all") {
if (!is_array($config['system']['group'][$gidx]['member']))
$config['system']['group'][$gidx]['member'] = array();
$config['system']['group'][$gidx]['member'][] = $new_user['uid'];
break;
}
}
$all_users = &$config['system']['user'];
$all_users[] = $new_user;
if (isset($groups))
local_user_set_groups($new_user, $groups);
local_user_set($new_user);
write_config();
conf_mount_ro();
echo "USER CREATED: " . $userargs['name'] . " \n";
exit(0);
}
// delete ou update : search the user...
if (!$user) {
echo "CANNOT " . strtoupper($action) . ": user '" . $userargs['name'] . "' is unknown\n";
exit(3);
}
unset($id);
$all_users = &$config['system']['user'];
foreach ($all_users as $iter_id=>$iter_user) {
if ($iter_user['name'] == $user['name']) {
$id = $iter_id;
}
}
if (!isset($id)) {
echo "CANNOT " . strtoupper($action) . ": cannot find user '" . $userargs['name'] . "' (please report this error)\n";
exit(3);
}
if ($action == "update") {
// encrypt password
if ($userargs['password'])
local_user_set_password($userargs, $userargs['password']);
else
unset($userargs['password']);
conf_mount_rw();
$updated_user = $all_users[$id];
// echo "BEFORE\n"; print_r($updated_user);
foreach ($userargs as $key=>$value)
$updated_user[$key] = $value;
if (!$userargs['disabled'])
unset($updated_user['disabled']);
// echo "AFTER\n"; print_r($updated_user);
$all_users[$id] = $updated_user;
if (isset($groups))
local_user_set_groups($updated_user, $groups);
local_user_set($updated_user);
write_config();
conf_mount_ro();
echo "UPDATED USER: " . $userargs['name'] . "\n";
exit(0);
}
if ($action == "delete") {
local_user_del($all_users[$id]);
unset($all_users[$id]);
write_config();
echo "DELETED USER: " . $userargs['name'] . "\n";
exit(0);
}
exit(1);
?>

View File

@ -0,0 +1,213 @@
import xml.etree.ElementTree as ET
try:
from django.conf import settings
PF_CONFIG_XML = settings.CONFIG_XML
CLEAR_PASSWORD_DIR = settings.CLEAR_PASSWORD_DIR
except ImportError:
PF_CONFIG_XML = '/conf/config.xml'
CLEAR_PASSWORD_DIR = '/var/db/univnautes/pfidp/passwords'
import datetime
import subprocess
import syslog
import htmlentitydefs
import re
import random
import fnmatch
import os
pattern = re.compile("&(\w+?);")
def html_entity_decode_char(m, defs=htmlentitydefs.entitydefs):
try:
return defs[m.group(1)]
except KeyError:
return m.group(0)
def html_entity_decode(string):
return pattern.sub(html_entity_decode_char, string)
def configxml():
f = open(PF_CONFIG_XML,'r')
root = ET.fromstring(f.read())
f.close()
return root
def create_password(username):
if not os.path.exists(CLEAR_PASSWORD_DIR):
os.makedirs(CLEAR_PASSWORD_DIR)
password = ''.join([random.choice('23456789ABCDEFGHJLMNPQRSTUVWXZabcdefghjkmnpqrstuvwxyz')
for x in range(random.randint(6,9))])
filename = os.path.join(CLEAR_PASSWORD_DIR, 'user-%s' % username)
f = open(filename, 'wb')
f.write(password)
f.close()
return password
def read_password(username):
filename = os.path.join(CLEAR_PASSWORD_DIR, 'user-%s' % username)
try:
f = open(filename, 'rb')
password = f.read()
f.close()
return password
except:
return None
def delete_password(username):
filename = os.path.join(CLEAR_PASSWORD_DIR, 'user-%s' % username)
try:
os.unlink(filename)
except:
pass
def get_all_pfusers(filter=None, with_password=False):
xml_users = configxml().findall('system/user')
if xml_users is None:
return {}
users = {}
for xml_user in xml_users:
scope = xml_user.find('scope')
if scope is None or scope.text != 'user':
continue
user = dict([(tag, xml_user.findtext(tag))
for tag in ('uid', 'name', 'expires', 'descr')])
if filter and not fnmatch.fnmatch(user['name'], filter+'*'):
continue
user['descr'] = html_entity_decode(user['descr']).decode('iso-8859-1')
user['priv'] = set([priv.text for priv in xml_user.findall('priv')])
if with_password:
user['password'] = read_password(user['name'])
expires = user.get('expires')
if expires:
try:
user['expires'] = datetime.datetime.strptime(expires, '%m/%d/%Y').date() # pfSense format is mm/dd/YYYY
user['ttl'] = (user['expires'] - datetime.date.today()).days
if user['ttl'] < 0:
user['ttl'] = 0
except:
# pfSense xml error ? ok, I suppose the account is expired
user['expires'] = datetime.date.today()
user['ttl'] = 0
else:
user['expires'] = None
user['ttl'] = -1 # no expiration
user['disabled'] = xml_user.find('disabled') is not None
users[user['uid']] = user
# priv from groups
xml_groups = configxml().findall('system/group')
if not xml_groups:
return users
for xml_group in xml_groups:
xml_members = xml_group.findall('member')
if not xml_members:
continue
xml_privs = xml_group.findall('priv')
if not xml_privs:
continue
privs = set([priv.text for priv in xml_privs])
for uid in [xml_member.text for xml_member in xml_members]:
user = users.get(uid)
if user:
user['priv'].update(privs)
# 1) keep only users with the "univnautes-idp" privilege
# 1bis) and without "univnautes-idp-admin" privilege
# 2) new index of the dict is the username (instead of the uid)
users = dict([(users[uid]['name'], users[uid]) for uid in users
if ('univnautes-idp' in users[uid]['priv']) and ('univnautes-idp-admin' not in users[uid]['priv'])])
return users
def call(action, **kwargs):
cmd = ['/usr/local/univnautes/idp/idp/users_admin/pf_useradm', ]
cmd.append(action)
cmd += [ '%s=%s' % (k,v.encode('iso-8859-1')) for k,v in kwargs.items() ]
try:
p = subprocess.Popen(cmd, close_fds=True,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
except OSError, e:
syslog.openlog("idpusersadmin/pf_useradmin", syslog.LOG_PID)
syslog.syslog(syslog.LOG_LOCAL4 | syslog.LOG_INFO,
"ERROR %s: OSError %s" % (action, e))
return False, "ERROR: OSError %s" % e
stdout, stderr = p.communicate()
if p.returncode != 0:
syslog.openlog("idpusersadmin/pf_useradmin", syslog.LOG_PID)
syslog.syslog(syslog.LOG_LOCAL4 | syslog.LOG_INFO,
"ERROR %s: code %s (out:%s) (err:%s)" % (action, p.returncode, stdout, stderr))
return False, stdout + stderr
syslog.openlog("idpusersadmin/pf_useradmin", syslog.LOG_PID)
syslog.syslog(syslog.LOG_LOCAL4 | syslog.LOG_INFO,
"SUCCESS %s: %s" % (action, stdout))
return True, stdout
def create(username, password, expires, disabled=False, descr='', multiple=False):
if isinstance(expires, datetime.date):
expires_str = expires.strftime('%m/%d/%Y')
else:
expires_str = ''
if disabled:
disabled_str='yes'
else:
disabled_str='no'
privs = 'univnautes-idp'
if multiple:
privs += ',univnautes-idp-multiple'
if not password:
password = create_password(username)
return call('create', name=username, password=password,
expires=expires_str,
disabled=disabled_str,
privs=privs,
descr=descr)
def desactivate(username):
return call('update', name=username, disabled='yes')
def activate(username):
return call('update', name=username, disabled='no')
def delete(username):
return call('delete', name=username)
def update(username, password, expires, disabled=False, descr='', multiple=False):
if isinstance(expires, datetime.date):
expires_str = expires.strftime('%m/%d/%Y')
else:
expires_str = ''
if disabled:
disabled_str='yes'
else:
disabled_str='no'
privs = 'univnautes-idp'
if multiple:
privs += ',univnautes-idp-multiple'
if password:
delete_password(username)
return call('update', name=username, password=password,
expires=expires_str,
disabled=disabled_str,
privs=privs,
descr=descr)
else:
return call('update', name=username,
expires=expires_str,
disabled=disabled_str,
privs=privs,
descr=descr)

View File

@ -0,0 +1,17 @@
from django.conf.urls.defaults import patterns
from django.contrib.auth.decorators import login_required
import views
urlpatterns = patterns('',
(r'^$', login_required(views.index)),
(r'^create$', login_required(views.create)),
(r'^read/(?P<name>[a-z0-9\.\-_]+)$', login_required(views.read)),
(r'^update/(?P<name>[a-z0-9\.\-_]+)$', login_required(views.update)),
(r'^delete/(?P<name>[a-z0-9\.\-_]+)$', login_required(views.delete)),
(r'^desactivate/(?P<name>[a-z0-9\.\-_]+)$', login_required(views.desactivate)),
(r'^activate/(?P<name>[a-z0-9\.\-_]+)$', login_required(views.activate)),
(r'^multiple$', login_required(views.multiple)),
(r'^import$', login_required(views.csv_import)),
)

View File

@ -0,0 +1,325 @@
# views for user admin
# -*- encoding: utf-8 -*-
import datetime
import csv
from django.conf import settings
from django.shortcuts import render_to_response, redirect
from django.contrib.auth.decorators import user_passes_test
from django.template import RequestContext, loader, Context
from django.contrib import messages
from django.http import HttpResponse
import pfusers
from .forms import UserForm, NewUserForm, ConfirmForm, UploadFileForm
@user_passes_test(lambda user: user.is_staff, login_url='/logout')
def index(request):
filter = request.GET.get('filter', None)
if filter is None:
filter = request.COOKIES.get('filter', None)
context = { 'users': pfusers.get_all_pfusers(filter),
'filter': filter or '' }
response = render_to_response('users_admin/index.html',
context,
context_instance=RequestContext(request))
if filter is not None:
response.set_cookie('filter', filter)
return response
@user_passes_test(lambda user: user.is_staff, login_url='/logout')
def create(request):
if request.method == 'POST':
form = NewUserForm(request.POST)
if form.is_valid():
expires = form.cleaned_data.get('expires')
name = form.cleaned_data.get('name')
password = form.cleaned_data.get('password')
descr = form.cleaned_data.get('descr')
disabled = form.cleaned_data.get('disabled')
multiple = form.cleaned_data.get('multiple')
userset_number = int(form.cleaned_data.get('userset_number'))
if userset_number == 1:
ret, log = pfusers.create(name, password=password, descr=descr,
expires=expires, disabled=disabled, multiple=multiple)
if ret:
messages.success(request, u'Utilisateur <%s> ajouté.' % name)
else:
messages.error(request, u'Erreur lors de la création <%s>: %s' % (name, log))
else: # (multiple users creation)
userset_start = int(form.cleaned_data.get('userset_start'))
for n in range(userset_start, userset_start+userset_number):
username = '%s-%d' % (name, n)
ret, log = pfusers.create(username, password=password, descr=descr,
expires=expires, disabled=disabled, multiple=multiple)
if not ret:
messages.error(request, u'Erreur lors de la création <%s>: %s' % (username, log))
break
messages.success(request, u'Utilisateurs <%s-%d> à <%s-%d> ajoutés.' % \
(name, userset_start, name, userset_start+userset_number-1))
return redirect('.')
else:
initial = {
'name': '',
'userset_number': 1,
'userset_start': 1,
}
dt = datetime.date.today() + datetime.timedelta(settings.IDP_UA_DEFAULT_EXPIRES)
initial['expires'] = dt.strftime('%d/%m/%Y')
form = NewUserForm(initial=initial)
return render_to_response('users_admin/create.html',
{ 'form': form, },
context_instance=RequestContext(request))
@user_passes_test(lambda user: user.is_staff, login_url='/logout')
def read(request, name=None):
user = pfusers.get_all_pfusers(with_password=True).get(name, None)
if user == None:
messages.error(request, u'Utilisateur <%s> inconnu.' % name)
return redirect('..')
return render_to_response('users_admin/read.html',
{ 'user': user, },
context_instance=RequestContext(request))
@user_passes_test(lambda user: user.is_staff, login_url='/logout')
def update(request, name=None):
user = pfusers.get_all_pfusers().get(name, None)
if user == None:
messages.error(request, u'Utilisateur <%s> inconnu.' % name)
return redirect('..')
if request.method == 'POST':
form = UserForm(request.POST)
if form.is_valid():
expires = form.cleaned_data.get('expires')
password = form.cleaned_data.get('password')
descr = form.cleaned_data.get('descr')
disabled = form.cleaned_data.get('disabled')
multiple = form.cleaned_data.get('multiple')
ret, log = pfusers.update(name, password=password, descr=descr, expires=expires,
disabled=disabled, multiple=multiple)
if ret:
messages.success(request, u'Utilisateur <%s> modifié.' % name)
return redirect('..')
else:
messages.error(request, u'Erreur lors de la modification de <%s>: %s' % (name, log))
return redirect('..')
else:
initial = {
'name': user['name'],
'descr': user['descr'],
'multiple': 'univnautes-idp-multiple' in user['priv'],
'disabled': user['disabled'],
}
if isinstance(user['expires'], datetime.date):
initial['expires'] = user['expires'].strftime('%d/%m/%Y')
form = UserForm(initial=initial)
return render_to_response('users_admin/update.html',
{ 'form': form, 'user': user, },
context_instance=RequestContext(request))
@user_passes_test(lambda user: user.is_staff, login_url='/logout')
def delete(request, name=None):
user = pfusers.get_all_pfusers().get(name, None)
if user == None:
messages.error(request, u'Utilisateur <%s> inconnu.' % name)
return redirect('..')
if request.method == 'POST':
form = ConfirmForm(request.POST)
if form.is_valid():
ret, log = pfusers.delete(name)
if ret:
messages.success(request, u'Utilisateur <%s> supprimé.' % name)
return redirect('..')
else:
messages.error(request, u'Erreur lors de la suppression de <%s>: %s' % (name, log))
return redirect('..')
form = ConfirmForm()
return render_to_response('users_admin/confirm.html',
{ 'form': form, 'users': [user] ,
'title': u"Supprimer le compte <%s> ?" % name },
context_instance=RequestContext(request))
@user_passes_test(lambda user: user.is_staff, login_url='/logout')
def desactivate(request, name=None):
user = pfusers.get_all_pfusers().get(name, None)
if user == None:
messages.error(request, u'Utilisateur <%s> inconnu.' % name)
return redirect('..')
if request.method == 'POST':
form = ConfirmForm(request.POST)
if form.is_valid():
ret, log = pfusers.desactivate(name)
if ret:
messages.success(request, u'Utilisateur <%s> désactivé.' % name)
return redirect('..')
else:
messages.error(request, u'Erreur lors de la désactivation de <%s>: %s' % (name, log))
return redirect('..')
form = ConfirmForm()
return render_to_response('users_admin/confirm.html',
{ 'form': form, 'users': [user] ,
'title': u"Désactiver le compte <%s> ?" % name },
context_instance=RequestContext(request))
@user_passes_test(lambda user: user.is_staff, login_url='/logout')
def activate(request, name=None):
user = pfusers.get_all_pfusers().get(name, None)
if user == None:
messages.error(request, u'Utilisateur <%s> inconnu.' % name)
return redirect('..')
if request.method == 'POST':
form = ConfirmForm(request.POST)
if form.is_valid():
ret, log = pfusers.activate(name)
if ret:
messages.success(request, u'Utilisateur <%s> activé.' % name)
return redirect('..')
else:
messages.error(request, u"Erreur lors de l'activation de <%s>: %s" % (name, log))
return redirect('..')
form = ConfirmForm()
return render_to_response('users_admin/confirm.html',
{ 'form': form, 'users': [user] ,
'title': u"Activer le compte <%s> ?" % name },
context_instance=RequestContext(request))
ACTION_NAME = {
'delete': u'Suppression',
'desactivate': u'Désactivation',
'activate': u'Activation',
'read': u'Afficher les utilisateurs',
'csv': u'Export CSV',
}
@user_passes_test(lambda user: user.is_staff, login_url='/logout')
def multiple(request):
if request.method == 'POST':
action = request.POST.get('action')
if action:
# we need a confirmation
if not action in ACTION_NAME:
messages.warning(request, u'Choisissez une action...')
return redirect('.')
else:
title = '%s de ces comptes ?' % ACTION_NAME[action]
names = request.POST.getlist('users')
if len(names) == 0:
messages.warning(request, u'Sélectionnez au moins un utilisateur.')
return redirect('.')
all_pfusers = pfusers.get_all_pfusers(with_password=(action in ['read','csv']))
try:
users = [ all_pfusers[name] for name in names ]
except KeyError:
messages.error(request, u'Au moins un utilisateur inconnu dans la liste.')
return redirect('.')
if action == "read":
return render_to_response('users_admin/read-list.html',
{ 'users': users, }, context_instance=RequestContext(request))
if action == "csv":
return csv_export(users)
request.session['univnautes_idpua_action'] = action
request.session['univnautes_idpua_names'] = names
form = ConfirmForm()
return render_to_response('users_admin/confirm.html',
{ 'form': form,
'users': users,
'title': title },
context_instance=RequestContext(request))
else:
# normally, it's a confirmation
form = ConfirmForm(request.POST)
if form.is_valid():
try:
names = request.session['univnautes_idpua_names']
action = request.session['univnautes_idpua_action']
except KeyError:
messages.error(request, u'Erreur dans la session !')
return redirect('.')
if not action in ACTION_NAME:
messages.error(request, u'Action invalide')
return redirect('.')
success = []
errors = []
for name in names:
ret, log = getattr(pfusers, action)(name)
if ret:
success.append(name)
else:
errors.append('%s (%s)' % (name, log))
if success:
messages.success(request, u'%s: %s' % (ACTION_NAME[action], ', '.join(success)))
if errors:
messages.error(request, u'ERREUR %s: %s' % (ACTION_NAME[action], ', '.join(errors)))
del request.session['univnautes_idpua_names']
del request.session['univnautes_idpua_action']
else:
messages.error('Erreur lors de la confirmation.')
return redirect('.')
def csv_export(users):
response = HttpResponse(content_type='text/csv')
response['Content-Disposition'] = 'attachment; filename="users.csv"'
t = loader.get_template('users_admin/users.csv')
c = Context({ 'users': users, })
response.write(t.render(c))
return response
@user_passes_test(lambda user: user.is_staff, login_url='/logout')
def csv_import(request):
if request.method == 'POST':
form = UploadFileForm(request.POST, request.FILES)
if form.is_valid():
try:
created_users, updated_users = import_csv_file(request.FILES['file'])
except Exception as e:
messages.error(request, u'Import du fichier impossible, erreur : %s' % e)
else:
messages.success(request, u'%d utilisateurs créés, %d mis à jour' % \
(created_users, updated_users))
return redirect('.')
else:
form = UploadFileForm()
return render_to_response('users_admin/import.html',
{'form': form},
context_instance=RequestContext(request))
def import_csv_file(f):
filename = '/var/tmp/users-import.csv'
with open(filename, 'wb+') as destination:
for chunk in f.chunks():
destination.write(chunk)
csvfile = open(filename, 'rb')
dialect = csv.Sniffer().sniff(csvfile.read(1024))
csvfile.seek(0)
reader = csv.reader(csvfile, dialect)
created_users = []
updated_users = []
all_pfusers = pfusers.get_all_pfusers()
# analyse all the file, then create (if no exception)
headers = reader.next()
for row in reader:
user = {
'username': row[0].strip(),
'descr': row[1].strip(),
'expires': datetime.datetime.strptime(row[2],'%Y-%m-%d').date(),
'password': row[3].strip() or None
}
if user['username'] in all_pfusers:
updated_users.append(user)
else:
created_users.append(user)
csvfile.close()
for user in created_users:
pfusers.create(**user)
for user in updated_users:
pfusers.update(**user)
return len(created_users), len(updated_users)

View File

@ -0,0 +1,30 @@
# -*- coding: utf-8 -*-
#
# UnivNautes
# Copyright (C) 2014 Entr'ouvert
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the GNU Affero General Public License as published by the Free
# Software Foundation, either version 3 of the License, or (at your option) any
# later version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
# details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import os
from django.conf import settings
from django.views.static import serve as django_static_serve
def static_serve(request, path):
# if path is "this/file.css", search for "captiveportal-idp-static-this-file.css"
custom_path = 'captiveportal-idp-static-%s' % path.replace('/', '-')
document_root = settings.CPELEMENTS
if os.path.exists(os.path.join(document_root, custom_path)):
return django_static_serve(request, custom_path, document_root=document_root)
return django_static_serve(request, path, document_root=settings.STATIC_ROOT)

View File

@ -0,0 +1,32 @@
"""
WSGI config for idp project.
This module contains the WSGI application used by Django's development server
and any production WSGI deployments. It should expose a module-level variable
named ``application``. Django's ``runserver`` and ``runfcgi`` commands discover
this application via the ``WSGI_APPLICATION`` setting.
Usually you will have the standard Django WSGI application here, but it also
might make sense to replace the whole Django WSGI application with a custom one
that later delegates to the Django one. For example, you could introduce WSGI
middleware here, or combine a Django application with an application of another
framework.
"""
import os
# We defer to a DJANGO_SETTINGS_MODULE already in the environment. This breaks
# if running multiple sites in the same mod_wsgi process. To fix this, use
# mod_wsgi daemon mode with each site in its own daemon process, or use
# os.environ["DJANGO_SETTINGS_MODULE"] = "idp.settings"
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "idp.settings")
# This application object is used by any WSGI server configured to use this
# file. This includes Django's development server, if the WSGI_APPLICATION
# setting points here.
from django.core.wsgi import get_wsgi_application
application = get_wsgi_application()
# Apply WSGI middleware here.
# from helloworld.wsgi import HelloWorldApplication
# application = HelloWorldApplication(application)

View File

@ -0,0 +1,169 @@
#
# lighttpd configuration file
#
# use a it as base for lighttpd 1.0.0 and above
#
############ Options you really have to take care of ####################
## FreeBSD!
server.event-handler = "freebsd-kqueue"
server.network-backend = "writev"
#server.use-ipv6 = "enable"
## modules to load
server.modules = ( "mod_access", "mod_expire", "mod_compress", "mod_redirect",
,"mod_rewrite","mod_evasive", "mod_fastcgi", "mod_setenv",
)
server.max-keep-alive-requests = 15
server.max-keep-alive-idle = 30
## a static document-root, for virtual-hosting take look at the
## server.virtual-* options
server.document-root = "/usr/local/univnautes/idp/www"
# Maximum idle time with nothing being written (php downloading)
server.max-write-idle = 999
## where to send error-messages to
server.errorlog-use-syslog="enable"
# files to check for if .../ is requested
server.indexfiles = ( "index.html" )
# mimetype mapping
mimetype.assign = (
".pdf" => "application/pdf",
".sig" => "application/pgp-signature",
".spl" => "application/futuresplash",
".class" => "application/octet-stream",
".ps" => "application/postscript",
".torrent" => "application/x-bittorrent",
".dvi" => "application/x-dvi",
".gz" => "application/x-gzip",
".pac" => "application/x-ns-proxy-autoconfig",
".swf" => "application/x-shockwave-flash",
".tar.gz" => "application/x-tgz",
".tgz" => "application/x-tgz",
".tar" => "application/x-tar",
".zip" => "application/zip",
".mp3" => "audio/mpeg",
".m3u" => "audio/x-mpegurl",
".wma" => "audio/x-ms-wma",
".wax" => "audio/x-ms-wax",
".ogg" => "audio/x-wav",
".wav" => "audio/x-wav",
".gif" => "image/gif",
".jpg" => "image/jpeg",
".jpeg" => "image/jpeg",
".png" => "image/png",
".xbm" => "image/x-xbitmap",
".xpm" => "image/x-xpixmap",
".xwd" => "image/x-xwindowdump",
".css" => "text/css",
".html" => "text/html",
".htm" => "text/html",
".js" => "text/javascript",
".asc" => "text/plain",
".c" => "text/plain",
".conf" => "text/plain",
".text" => "text/plain",
".txt" => "text/plain",
".dtd" => "text/xml",
".xml" => "text/xml",
".mpeg" => "video/mpeg",
".mpg" => "video/mpeg",
".mov" => "video/quicktime",
".qt" => "video/quicktime",
".avi" => "video/x-msvideo",
".asf" => "video/x-ms-asf",
".asx" => "video/x-ms-asf",
".wmv" => "video/x-ms-wmv",
".bz2" => "application/x-bzip",
".tbz" => "application/x-bzip-compressed-tar",
".tar.bz2" => "application/x-bzip-compressed-tar"
)
# Use the "Content-Type" extended attribute to obtain mime type if possible
#mimetypes.use-xattr = "enable"
## deny access the file-extensions
#
# ~ is for backupfiles from vi, emacs, joe, ...
# .inc is often used for code includes which should in general not be part
# of the document-root
url.access-deny = ( "~", ".inc" )
######### Options that are good to be but not neccesary to be changed #######
## bind to port (default: 80)
server.bind = "0.0.0.0"
server.port = 4443
$SERVER["socket"] == "0.0.0.0:4443" { }
$SERVER["socket"] == "[::]:4443" {
## ssl configuration
ssl.engine = "enable"
ssl.pemfile = "/var/etc/cert-univnautes-portal.pem"
ssl.ca-file = "/var/etc/ca-univnautes-portal.pem"
}
## error-handler for status 404
#server.error-handler-404 = "/error-handler.html"
#server.error-handler-404 = "/error-handler.php"
## to help the rc.scripts
server.pid-file = "/var/run/lighty-idp.pid"
## virtual directory listings
server.dir-listing = "disable"
## enable debugging
debug.log-request-header = "disable"
debug.log-response-header = "disable"
debug.log-request-handling = "disable"
debug.log-file-not-found = "disable"
# gzip compression
compress.cache-dir = "/tmp/lighttpdcompress/"
compress.filetype = ("text/plain","text/css", "text/xml", "text/javascript" )
server.max-request-size = 384
#### fastcgi module
fastcgi.server = (
"/django.fcgi" => (
"main" => (
"socket" => "/tmp/univnautes-idp-fcgi.sock",
"check-local" => "disable",
)
),
)
url.rewrite-if-not-file = (
"^/map/(.*)$" => "/django.fcgi/proxymap/$1",
)
url.rewrite-once = (
"^/favicon\.ico$" => "/static/favicon.ico",
"^/*$" => "/django.fcgi/",
"^/(.*)$" => "/django.fcgi/$1",
)
evasive.max-conns-per-ip = 64
expire.url = (
"" => "access 50 hours",
)
## ssl configuration
ssl.engine = "enable"
ssl.pemfile = "/var/etc/cert-univnautes-portal.pem"
ssl.use-sslv2 = "disable"
ssl.use-sslv3 = "disable"
ssl.cipher-list = "DHE-RSA-CAMELLIA256-SHA:DHE-DSS-CAMELLIA256-SHA:CAMELLIA256-SHA:DHE-DSS-AES256-SHA:AES256-SHA:DHE-RSA-CAMELLIA128-SHA:DHE-DSS-CAMELLIA128-SHA:CAMELLIA128-SHA:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA:AES128-SHA:RC4-SHA:RC4-MD5:!aNULL:!eNULL:!3DES:@STRENGTH"
ssl.ca-file = "/var/etc/ca-univnautes-portal.pem"

View File

@ -0,0 +1,10 @@
#!/usr/bin/env python
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "idp.settings")
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)

89
usr/local/univnautes/idp/rc.sh Executable file
View File

@ -0,0 +1,89 @@
#!/usr/local/bin/bash
DB=/var/db/univnautes-idp.sqlite3
IDPDIR=/usr/local/univnautes/idp
cd $IDPDIR
function cronstop() {
for cron in idp-update-metadatas
do
if [ -r /var/run/${cron}-cron.pid ]
then
PID=`cat /var/run/${cron}-cron.pid`
ps waux | grep "$PID" | grep minicron | grep -vq grep && kill $PID
rm -f /var/run/${cron}-cron.pid
fi
done
}
function cronstart() {
cronstop
/usr/local/bin/minicron 3600 /var/run/idp-update-metadatas-cron.pid $IDPDIR/idp-update-metadatas.sh
}
function lightystop() {
if [ -r /var/run/lighty-idp.pid ]
then
PID=`cat /var/run/lighty-idp.pid`
ps waux | grep "$PID" | grep lighty-idp | grep -vq grep && kill $PID
rm -f /var/run/lighty-idp.pid
fi
}
function lightystart() {
lightystop
/usr/local/sbin/lighttpd -f /usr/local/univnautes/idp/lighty-idp.conf
}
function syncdata() {
echo "sync metadatas in progress (backgrounded)" | logger -p local4.info -t idp/syncdata
(
cd /usr/local/univnautes/idp/
./idp-update-metadatas.sh | logger -p local4.info -t idp/idp-update-metadatas
) &
}
function syncdb() {
if ! test -r $DB
then
python manage.py syncdb --noinput --no-initial-data | logger -p local4.info -t idp/syncdb
python manage.py loaddata fixtures/* | logger -p local4.info -t idp/loaddata
fi
}
function start() {
if python manage.py configxml get idp > /dev/null
then
syncdb
python manage.py collectstatic -v0 -l --noinput | logger -p local4.info -t idp/collectstatic
python manage.py runfcgi socket=/tmp/univnautes-idp-fcgi.sock method=prefork daemonize=true pidfile=/var/run/univnautes-idp-fcgi.pid
echo "started (manage.py runfcgi)" | logger -p local4.info -t idp/start
syncdata
cronstart
lightystart
fi
}
function stop() {
lightystop
cronstop
kill $(cat /var/run/univnautes-idp-fcgi.pid)
echo "stopped (kill)" | logger -p local4.info -t idp/stop
}
function restart() {
stop
sleep 1
start
}
function status() {
ps waux | grep $(cat /var/run/univnautes-idp-fcgi.pid) | grep -v grep
}
echo $1 | logger -p local4.info -t idp/rc
$1