django-cas-server-1.1.0/0000755000175000017500000000000013436457571016524 5ustar valentinvalentin00000000000000django-cas-server-1.1.0/cas_server/0000755000175000017500000000000013436457571020660 5ustar valentinvalentin00000000000000django-cas-server-1.1.0/cas_server/locale/0000755000175000017500000000000013436457571022117 5ustar valentinvalentin00000000000000django-cas-server-1.1.0/cas_server/locale/fr/0000755000175000017500000000000013436457571022526 5ustar valentinvalentin00000000000000django-cas-server-1.1.0/cas_server/locale/fr/LC_MESSAGES/0000755000175000017500000000000013436457571024313 5ustar valentinvalentin00000000000000django-cas-server-1.1.0/cas_server/locale/fr/LC_MESSAGES/django.po0000644000175000017500000003303113060003340026063 0ustar valentinvalentin00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: cas_server\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2016-09-18 11:29+0200\n" "PO-Revision-Date: 2016-09-18 11:30+0200\n" "Last-Translator: Valentin Samir \n" "Language-Team: django \n" "Language: fr\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" "X-Generator: Poedit 1.8.9\n" #: apps.py:25 templates/cas_server/base.html:7 #: templates/cas_server/base.html:26 msgid "Central Authentication Service" msgstr "Service Central d'Authentification" #: default_settings.py:197 msgid "" "The Central Authentication Service grants you access to most of our websites " "by authenticating only once, so you don't need to type your credentials " "again unless your session expires or you logout." msgstr "" "Le Service Central d'Authentification permet, en vous authentifiant une " "seule fois, d'accéder à la plupart de nos sites sans avoir à retaper votre " "identifiant et votre mot de passe chaque fois que vous changez de site, " "jusqu'à ce que votre session expire ou que vous vous déconnectiez." #: forms.py:85 msgid "Identity provider" msgstr "fournisseur d'identité" #: forms.py:89 forms.py:111 msgid "Warn me before logging me into other sites." msgstr "Prévenez-moi avant d'accéder à d'autres services." #: forms.py:93 msgid "Remember the identity provider" msgstr "Se souvenir du fournisseur d'identité" #: forms.py:104 models.py:594 msgid "username" msgstr "nom d'utilisateur" #: forms.py:108 msgid "password" msgstr "mot de passe" #: forms.py:131 msgid "The credentials you provided cannot be determined to be authentic." msgstr "Les informations transmises n'ont pas permis de vous authentifier." #: forms.py:183 msgid "User not found in the temporary database, please try to reconnect" msgstr "" "Utilisateur non trouvé dans la base de donnée temporaire, essayez de vous " "reconnecter" #: forms.py:197 msgid "service" msgstr "service" #: management/commands/cas_clean_federate.py:20 msgid "Clean old federated users" msgstr "Nettoyer les anciens utilisateurs fédérés" #: management/commands/cas_clean_sessions.py:22 msgid "Clean deleted sessions" msgstr "Nettoyer les sessions supprimées" #: management/commands/cas_clean_tickets.py:22 msgid "Clean old tickets" msgstr "Nettoyer les vieux tickets" #: models.py:71 msgid "identity provider" msgstr "fournisseur d'identité" #: models.py:72 msgid "identity providers" msgstr "fournisseurs d'identités" #: models.py:78 msgid "suffix" msgstr "suffixe" #: models.py:80 msgid "" "Suffix append to backend CAS returned username: ``returned_username`` @ " "``suffix``." msgstr "" "Suffixe ajouté au nom d'utilisateur retourné par le CAS du fournisseur " "d'identité : `nom retourné`@`suffixe`." #: models.py:87 msgid "server url" msgstr "url du serveur" #: models.py:97 msgid "CAS protocol version" msgstr "Version du protocole CAS" #: models.py:99 msgid "" "Version of the CAS protocol to use when sending requests the the backend CAS." msgstr "" "Version du protocole CAS à utiliser lorsque l'on envoie des requête au CAS " "du fournisseur d'identité." #: models.py:106 msgid "verbose name" msgstr "Nom du fournisseur" #: models.py:107 msgid "Name for this identity provider displayed on the login page." msgstr "Nom affiché pour ce fournisseur d'identité sur la page de connexion." #: models.py:113 models.py:446 msgid "position" msgstr "position" #: models.py:127 msgid "display" msgstr "afficher" #: models.py:128 msgid "Display the provider on the login page." msgstr "Afficher le fournisseur d'identité sur la page de connexion." #: models.py:245 msgid "User" msgstr "Utilisateur" #: models.py:246 msgid "Users" msgstr "Utilisateurs" #: models.py:320 #, python-format msgid "Error during service logout %s" msgstr "Une erreur est survenue durant la déconnexion du service %s" #: models.py:440 msgid "Service pattern" msgstr "Motif de service" #: models.py:441 msgid "Services patterns" msgstr "Motifs de services" #: models.py:447 msgid "service patterns are sorted using the position attribute" msgstr "Les motifs de service sont trié selon l'attribut position" #: models.py:455 models.py:620 msgid "name" msgstr "nom" #: models.py:456 msgid "A name for the service" msgstr "Un nom pour le service" #: models.py:464 models.py:663 models.py:693 msgid "pattern" msgstr "motif" #: models.py:466 msgid "" "A regular expression matching services. Will usually looks like '^https://" "some\\.server\\.com/path/.*$'.As it is a regular expression, special " "character must be escaped with a '\\'." msgstr "" "Une expression rationnelle reconnaissant un service. Devrait généralement " "ressemblé à '^https://un\\.serveur\\.fr/chemin/.*$'. Comme il s'agit d'une " "expression rationnelle, les caractères spéciaux doivent être échappés avec " "un '\\'." #: models.py:477 msgid "user field" msgstr "champ utilisateur" #: models.py:478 msgid "Name of the attribute to transmit as username, empty = login" msgstr "" "Nom de l'attribut devant être transmis comme nom d'utilisateur au service. " "vide = nom de connexion" #: models.py:483 msgid "restrict username" msgstr "limiter les noms d'utilisateurs" #: models.py:484 msgid "Limit username allowed to connect to the list provided bellow" msgstr "" "Limiter les noms d'utilisateurs autorisé à se connecter à la liste fournie " "ci-dessous" #: models.py:489 msgid "proxy" msgstr "proxy" #: models.py:490 msgid "Proxy tickets can be delivered to the service" msgstr "des proxy tickets peuvent être délivrés au service" #: models.py:496 msgid "proxy callback" msgstr "" #: models.py:497 msgid "can be used as a proxy callback to deliver PGT" msgstr "peut être utilisé comme un callback pour recevoir un PGT" #: models.py:504 msgid "single log out" msgstr "" #: models.py:505 msgid "Enable SLO for the service" msgstr "Active le SLO pour le service" #: models.py:513 msgid "single log out callback" msgstr "" #: models.py:514 msgid "" "URL where the SLO request will be POST. empty = service url\n" "This is usefull for non HTTP proxied services." msgstr "" "URL à laquelle la requête de déconnexion sera postée. vide = l'url du " "service\n" "Ceci n'est en général utilisé que pour des services non HTTP proxifiés" #: models.py:595 msgid "username allowed to connect to the service" msgstr "noms d'utilisateurs autorisés à se connecter au service" #: models.py:621 msgid "name of an attribute to send to the service, use * for all attributes" msgstr "" "nom d'un attribut à envoyer au service, utiliser * pour tous les attributs" #: models.py:628 models.py:701 msgid "replace" msgstr "remplacement" #: models.py:629 msgid "" "name under which the attribute will be show to the service. empty = default " "name of the attribut" msgstr "" "nom sous lequel l'attribut sera rendu visible au service. vide = inchangé" #: models.py:656 models.py:687 msgid "attribute" msgstr "attribut" #: models.py:657 msgid "Name of the attribute which must verify pattern" msgstr "Nom de l'attribut devant vérifier un motif" #: models.py:664 msgid "a regular expression" msgstr "une expression régulière" #: models.py:688 msgid "Name of the attribute for which the value must be replace" msgstr "Nom de l'attribut pour lequel la valeur doit être remplacé" #: models.py:694 msgid "An regular expression maching whats need to be replaced" msgstr "Une expression régulière reconnaissant ce qui doit être remplacé" #: models.py:702 msgid "replace expression, groups are capture by \\1, \\2 …" msgstr "expression de remplacement, les groupe sont capturé par \\1, \\2" #: templates/cas_server/base.html:43 #, python-format msgid "" "A new version of the application is available. This instance runs " "%(VERSION)s and the last version is %(LAST_VERSION)s. Please consider " "upgrading." msgstr "" "Une nouvelle version de l'application est disponible. Cette instance utilise " "la version %(VERSION)s et la dernière version est %(LAST_VERSION)s. Merci de " "vous mettre à jour." #: templates/cas_server/logged.html:4 msgid "" "

Log In Successful

You have successfully logged into the Central " "Authentication Service.
For security reasons, please Log Out and Exit " "your web browser when you are done accessing services that require " "authentication!" msgstr "" "

Connexion réussie

Vous vous êtes connecté(e) auprès du Service " "Central d'Authentification.
Pour des raisons de sécurité, veuillez vous " "déconnecter et fermer votre navigateur après avoir fini d'accéder à des " "services demandant une authentification !" #: templates/cas_server/logged.html:8 msgid "Log me out from all my sessions" msgstr "Me déconnecter de toutes mes sessions" #: templates/cas_server/logged.html:14 msgid "Forget the identity provider" msgstr "Oublier le fournisseur d'identité" #: templates/cas_server/logged.html:18 msgid "Logout" msgstr "Se déconnecter" #: templates/cas_server/login.html:6 msgid "Please log in" msgstr "Veuillez vous authentifier" #: templates/cas_server/login.html:14 msgid "Login" msgstr "Connexion" #: templates/cas_server/warn.html:9 msgid "Connect to the service" msgstr "Se connecter au service" #: utils.py:744 #, python-format msgid "\"%(value)s\" is not a valid regular expression" msgstr "\"%(value)s\" n'est pas une expression rationnelle valide" #: views.py:185 msgid "" "

Logout successful

You have successfully logged out from the Central " "Authentication Service. For security reasons, close your web browser." msgstr "" "

Déconnexion réussie

Vous vous êtes déconnecté(e) du Service Central " "d'Authentification. Pour des raisons de sécurité, veuillez fermer votre " "navigateur." #: views.py:191 #, python-format msgid "" "

Logout successful

You have successfully logged out from %s sessions " "of the Central Authentication Service. For security reasons, close your web " "browser." msgstr "" "

Déconnexion réussie

Vous vous êtes déconnecté(e) de %s sessions du " "Service Central d'Authentification. Pour des raisons de sécurité, veuillez " "fermer votre navigateur." #: views.py:198 msgid "" "

Logout successful

You were already logged out from the Central " "Authentication Service. For security reasons, close your web browser." msgstr "" "

Déconnexion réussie

Vous étiez déjà déconnecté(e) du Service Central " "d'Authentification. Pour des raisons de sécurité, veuillez fermer votre " "navigateur." #: views.py:378 #, python-format msgid "" "Invalid response from your identity provider CAS upon ticket %(ticket)s " "validation: %(error)r" msgstr "" "Réponse invalide du CAS du fournisseur d'identité lors de la validation du " "ticket %(ticket)s: %(error)r" #: views.py:500 msgid "Invalid login ticket, please try to log in again" msgstr "Ticket de connexion invalide, merci de réessayé de vous connecter" #: views.py:692 #, python-format msgid "Authentication has been required by service %(name)s (%(url)s)" msgstr "" "Une demande d'authentification a été émise pour le service %(name)s " "(%(url)s)." #: views.py:730 #, python-format msgid "Service %(url)s not allowed." msgstr "le service %(url)s n'est pas autorisé." #: views.py:737 msgid "Username not allowed" msgstr "Nom d'utilisateur non authorisé" #: views.py:744 msgid "User characteristics not allowed" msgstr "Caractéristique utilisateur non autorisée" #: views.py:751 #, python-format msgid "The attribute %(field)s is needed to use that service" msgstr "L'attribut %(field)s est nécessaire pour se connecter à ce service" #: views.py:841 #, python-format msgid "Authentication renewal required by service %(name)s (%(url)s)." msgstr "Demande de réauthentification pour le service %(name)s (%(url)s)." #: views.py:848 #, python-format msgid "Authentication required by service %(name)s (%(url)s)." msgstr "Authentification requise par le service %(name)s (%(url)s)." #: views.py:855 #, python-format msgid "Service %s not allowed" msgstr "Le service %s n'est pas autorisé" #~ msgid "Logged" #~ msgstr "" #~ "

Connexion réussie

Vous vous êtes authentifié(e) auprès du Service " #~ "Central d'Authentification.
Pour des raisons de sécurité, veuillez " #~ "vous déconnecter et fermer votre navigateur lorsque vous avez fini " #~ "d'accéder aux services authentifiés." #~ msgid "warn" #~ msgstr "Prévenez-moi avant d'accéder à d'autres services." #~ msgid "login" #~ msgstr "Identifiant" #~ msgid "Bad user" #~ msgstr "Les informations transmises n'ont pas permis de vous authentifier." #~ msgid "" #~ "Error during service logout %(service)s:\n" #~ "%(error)s" #~ msgstr "" #~ "Une erreur est survenue durant la déconnexion du service %(service)s:" #~ "%(error)s" #~ msgid "Successfully logout" #~ msgstr "" #~ "

Déconnexion réussie

\n" #~ "Vous vous êtes déconnecté(e) du Service Central d'Authentification.
Pour des raisons de sécurité, veuillez fermer votre navigateur." #~ msgid "" #~ "A ProxyGrantingTicket can be delivered to the service in order to " #~ "authenticate for the user on a backend service" #~ msgstr "" #~ "Un ProxyGrantingTicket peut être délivré au service pour lui permettre de " #~ "s'authentifier en temps l'utilisateur à un autre service" #~ msgid "" #~ "Une demande d'authentification a été émise pour le service %(name)s " #~ "(%(url)s)" #~ msgstr "" #~ "Une demande d'authentification a été émise pour le service %(name)s " #~ "(%(url)s)" django-cas-server-1.1.0/cas_server/locale/fr/LC_MESSAGES/django.mo0000644000175000017500000002364713060003340026074 0ustar valentinvalentin00000000000000Mg-=\ s  7 > >0 6o       '4 \ w   0 ] =T    < 9 <6/s -2BST5nBjR WAxM+#O d.nE`v4 81 8C*L w7L[by)DQ]B;."G!j,=*<H"CiXn& F<Oc+ 57 &m ' !   q!"t!D"B"# #+#W# L$ m$hz$4$%3%:<%w%%%%K%J& M&Z&`&i& o&?|&&&&:&.'6'H'9Z''7@:DAHFJ' +3 )51= 2 6I"4?L*8(#0.<B>KG%E!-/9,&M;$ C"%(value)s" is not a valid regular expression

Log In Successful

You have successfully logged into the Central Authentication Service.
For security reasons, please Log Out and Exit your web browser when you are done accessing services that require authentication!

Logout successful

You have successfully logged out from %s sessions of the Central Authentication Service. For security reasons, close your web browser.

Logout successful

You have successfully logged out from the Central Authentication Service. For security reasons, close your web browser.

Logout successful

You were already logged out from the Central Authentication Service. For security reasons, close your web browser.A name for the serviceA new version of the application is available. This instance runs %(VERSION)s and the last version is %(LAST_VERSION)s. Please consider upgrading.A regular expression matching services. Will usually looks like '^https://some\.server\.com/path/.*$'.As it is a regular expression, special character must be escaped with a '\'.An regular expression maching whats need to be replacedAuthentication has been required by service %(name)s (%(url)s)Authentication renewal required by service %(name)s (%(url)s).Authentication required by service %(name)s (%(url)s).CAS protocol versionCentral Authentication ServiceClean deleted sessionsClean old federated usersClean old ticketsConnect to the serviceDisplay the provider on the login page.Enable SLO for the serviceError during service logout %sForget the identity providerIdentity providerInvalid login ticket, please try to log in againInvalid response from your identity provider CAS upon ticket %(ticket)s validation: %(error)rLimit username allowed to connect to the list provided bellowLog me out from all my sessionsLoginLogoutName for this identity provider displayed on the login page.Name of the attribute for which the value must be replaceName of the attribute to transmit as username, empty = loginName of the attribute which must verify patternPlease log inProxy tickets can be delivered to the serviceRemember the identity providerService %(url)s not allowed.Service %s not allowedService patternServices patternsSuffix append to backend CAS returned username: ``returned_username`` @ ``suffix``.The Central Authentication Service grants you access to most of our websites by authenticating only once, so you don't need to type your credentials again unless your session expires or you logout.The attribute %(field)s is needed to use that serviceThe credentials you provided cannot be determined to be authentic.URL where the SLO request will be POST. empty = service url This is usefull for non HTTP proxied services.UserUser characteristics not allowedUser not found in the temporary database, please try to reconnectUsername not allowedUsersVersion of the CAS protocol to use when sending requests the the backend CAS.Warn me before logging me into other sites.a regular expressionattributecan be used as a proxy callback to deliver PGTdisplayidentity provideridentity providersnamename of an attribute to send to the service, use * for all attributesname under which the attribute will be show to the service. empty = default name of the attributpasswordpatternpositionproxyreplacereplace expression, groups are capture by \1, \2 …restrict usernameserver urlserviceservice patterns are sorted using the position attributesuffixuser fieldusernameusername allowed to connect to the serviceverbose nameProject-Id-Version: cas_server Report-Msgid-Bugs-To: POT-Creation-Date: 2016-09-18 11:29+0200 PO-Revision-Date: 2016-09-18 11:30+0200 Last-Translator: Valentin Samir Language-Team: django Language: fr MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n > 1); X-Generator: Poedit 1.8.9 "%(value)s" n'est pas une expression rationnelle valide

Connexion réussie

Vous vous êtes connecté(e) auprès du Service Central d'Authentification.
Pour des raisons de sécurité, veuillez vous déconnecter et fermer votre navigateur après avoir fini d'accéder à des services demandant une authentification !

Déconnexion réussie

Vous vous êtes déconnecté(e) de %s sessions du Service Central d'Authentification. Pour des raisons de sécurité, veuillez fermer votre navigateur.

Déconnexion réussie

Vous vous êtes déconnecté(e) du Service Central d'Authentification. Pour des raisons de sécurité, veuillez fermer votre navigateur.

Déconnexion réussie

Vous étiez déjà déconnecté(e) du Service Central d'Authentification. Pour des raisons de sécurité, veuillez fermer votre navigateur.Un nom pour le serviceUne nouvelle version de l'application est disponible. Cette instance utilise la version %(VERSION)s et la dernière version est %(LAST_VERSION)s. Merci de vous mettre à jour.Une expression rationnelle reconnaissant un service. Devrait généralement ressemblé à '^https://un\.serveur\.fr/chemin/.*$'. Comme il s'agit d'une expression rationnelle, les caractères spéciaux doivent être échappés avec un '\'.Une expression régulière reconnaissant ce qui doit être remplacéUne demande d'authentification a été émise pour le service %(name)s (%(url)s).Demande de réauthentification pour le service %(name)s (%(url)s).Authentification requise par le service %(name)s (%(url)s).Version du protocole CASService Central d'AuthentificationNettoyer les sessions suppriméesNettoyer les anciens utilisateurs fédérésNettoyer les vieux ticketsSe connecter au serviceAfficher le fournisseur d'identité sur la page de connexion.Active le SLO pour le serviceUne erreur est survenue durant la déconnexion du service %sOublier le fournisseur d'identitéfournisseur d'identitéTicket de connexion invalide, merci de réessayé de vous connecterRéponse invalide du CAS du fournisseur d'identité lors de la validation du ticket %(ticket)s: %(error)rLimiter les noms d'utilisateurs autorisé à se connecter à la liste fournie ci-dessousMe déconnecter de toutes mes sessionsConnexionSe déconnecterNom affiché pour ce fournisseur d'identité sur la page de connexion.Nom de l'attribut pour lequel la valeur doit être remplacéNom de l'attribut devant être transmis comme nom d'utilisateur au service. vide = nom de connexionNom de l'attribut devant vérifier un motifVeuillez vous authentifierdes proxy tickets peuvent être délivrés au serviceSe souvenir du fournisseur d'identitéle service %(url)s n'est pas autorisé.Le service %s n'est pas autoriséMotif de serviceMotifs de servicesSuffixe ajouté au nom d'utilisateur retourné par le CAS du fournisseur d'identité : `nom retourné`@`suffixe`.Le Service Central d'Authentification permet, en vous authentifiant une seule fois, d'accéder à la plupart de nos sites sans avoir à retaper votre identifiant et votre mot de passe chaque fois que vous changez de site, jusqu'à ce que votre session expire ou que vous vous déconnectiez.L'attribut %(field)s est nécessaire pour se connecter à ce serviceLes informations transmises n'ont pas permis de vous authentifier.URL à laquelle la requête de déconnexion sera postée. vide = l'url du service Ceci n'est en général utilisé que pour des services non HTTP proxifiésUtilisateurCaractéristique utilisateur non autoriséeUtilisateur non trouvé dans la base de donnée temporaire, essayez de vous reconnecterNom d'utilisateur non authoriséUtilisateursVersion du protocole CAS à utiliser lorsque l'on envoie des requête au CAS du fournisseur d'identité.Prévenez-moi avant d'accéder à d'autres services.une expression régulièreattributpeut être utilisé comme un callback pour recevoir un PGTafficherfournisseur d'identitéfournisseurs d'identitésnomnom d'un attribut à envoyer au service, utiliser * pour tous les attributsnom sous lequel l'attribut sera rendu visible au service. vide = inchangémot de passemotifpositionproxyremplacementexpression de remplacement, les groupe sont capturé par \1, \2limiter les noms d'utilisateursurl du serveurserviceLes motifs de service sont trié selon l'attribut positionsuffixechamp utilisateurnom d'utilisateurnoms d'utilisateurs autorisés à se connecter au serviceNom du fournisseurdjango-cas-server-1.1.0/cas_server/locale/pt_BR/0000755000175000017500000000000013436457571023125 5ustar valentinvalentin00000000000000django-cas-server-1.1.0/cas_server/locale/pt_BR/django.po0000644000175000017500000002574113203573210024715 0ustar valentinvalentin00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # msgid "" msgstr "" "Project-Id-Version: \n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2017-08-22 08:18-0300\n" "PO-Revision-Date: 2017-08-29 18:09+0200\n" "Language-Team: Roberto Morati \n" "Language: pt_BR\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n > 1);\n" "Last-Translator: Valentin Samir \n" "X-Generator: Poedit 1.8.11\n" #: cas_server/apps.py:25 cas_server/templates/cas_server/base.html:7 #: cas_server/templates/cas_server/base.html:26 msgid "Central Authentication Service" msgstr "Central de Autenticação de Serviços" #: cas_server/default_settings.py:201 msgid "" "The Central Authentication Service grants you access to most of our websites by " "authenticating only once, so you don't need to type your credentials again unless your " "session expires or you logout." msgstr "" "A Central de Autenticação de Serviços garante seu acesso à maioria dos nossos sitespor " "meio de uma única autenticação, então você não precisa digitar suas " "credenciaisnovamente, ao menos que sua sessão expire ou seu logout." #: cas_server/forms.py:85 msgid "Identity provider" msgstr "Provedor de identidade" #: cas_server/forms.py:89 cas_server/forms.py:111 msgid "Warn me before logging me into other sites." msgstr "Avise-me antes de me registrar em outros sites" #: cas_server/forms.py:93 msgid "Remember the identity provider" msgstr "Relembrar o provedor de identidade" #: cas_server/forms.py:104 cas_server/models.py:638 msgid "username" msgstr "usuário" #: cas_server/forms.py:108 msgid "password" msgstr "senha" #: cas_server/forms.py:131 msgid "The credentials you provided cannot be determined to be authentic." msgstr "As credenciais que você forneceu não podem ser determinadas como autênticas." #: cas_server/forms.py:183 msgid "User not found in the temporary database, please try to reconnect" msgstr "Usuário não encontrado na base de dados temporária, por favor, tente se reconectar" #: cas_server/forms.py:197 msgid "service" msgstr "" #: cas_server/management/commands/cas_clean_federate.py:20 msgid "Clean old federated users" msgstr "" #: cas_server/management/commands/cas_clean_sessions.py:22 msgid "Clean deleted sessions" msgstr "" #: cas_server/management/commands/cas_clean_tickets.py:22 msgid "Clean old tickets" msgstr "" #: cas_server/models.py:71 msgid "identity provider" msgstr "provedor de identidade" #: cas_server/models.py:72 msgid "identity providers" msgstr "provedores de identidade" #: cas_server/models.py:78 msgid "suffix" msgstr "" #: cas_server/models.py:80 msgid "Suffix append to backend CAS returned username: ``returned_username`` @ ``suffix``." msgstr "" #: cas_server/models.py:87 msgid "server url" msgstr "" #: cas_server/models.py:97 msgid "CAS protocol version" msgstr "" #: cas_server/models.py:99 msgid "Version of the CAS protocol to use when sending requests the the backend CAS." msgstr "" #: cas_server/models.py:106 msgid "verbose name" msgstr "" #: cas_server/models.py:107 msgid "Name for this identity provider displayed on the login page." msgstr "Nome para exibir o provedor de identidade na página de login." #: cas_server/models.py:113 cas_server/models.py:490 msgid "position" msgstr "" #: cas_server/models.py:127 msgid "display" msgstr "" #: cas_server/models.py:128 msgid "Display the provider on the login page." msgstr "" #: cas_server/models.py:166 msgid "Federated user" msgstr "" #: cas_server/models.py:167 msgid "Federated users" msgstr "" #: cas_server/models.py:246 msgid "User attributes cache" msgstr "" #: cas_server/models.py:247 msgid "User attributes caches" msgstr "" #: cas_server/models.py:271 msgid "User" msgstr "" #: cas_server/models.py:272 msgid "Users" msgstr "" #: cas_server/models.py:364 #, python-format msgid "Error during service logout %s" msgstr "" #: cas_server/models.py:484 msgid "Service pattern" msgstr "" #: cas_server/models.py:485 msgid "Services patterns" msgstr "" #: cas_server/models.py:491 msgid "service patterns are sorted using the position attribute" msgstr "" #: cas_server/models.py:499 cas_server/models.py:664 msgid "name" msgstr "" #: cas_server/models.py:500 msgid "A name for the service" msgstr "" #: cas_server/models.py:508 cas_server/models.py:707 cas_server/models.py:737 msgid "pattern" msgstr "" #: cas_server/models.py:510 msgid "" "A regular expression matching services. Will usually looks like '^https://some\\.server" "\\.com/path/.*$'.As it is a regular expression, special character must be escaped with a " "'\\'." msgstr "" #: cas_server/models.py:521 msgid "user field" msgstr "" #: cas_server/models.py:522 msgid "Name of the attribute to transmit as username, empty = login" msgstr "" #: cas_server/models.py:527 msgid "restrict username" msgstr "" #: cas_server/models.py:528 msgid "Limit username allowed to connect to the list provided bellow" msgstr "" #: cas_server/models.py:533 msgid "proxy" msgstr "" #: cas_server/models.py:534 msgid "Proxy tickets can be delivered to the service" msgstr "" #: cas_server/models.py:540 msgid "proxy callback" msgstr "" #: cas_server/models.py:541 msgid "can be used as a proxy callback to deliver PGT" msgstr "" #: cas_server/models.py:548 msgid "single log out" msgstr "" #: cas_server/models.py:549 msgid "Enable SLO for the service" msgstr "" #: cas_server/models.py:558 msgid "" "URL where the SLO request will be POST. empty = service url\n" "This is usefull for non HTTP proxied services." msgstr "" #: cas_server/models.py:639 msgid "username allowed to connect to the service" msgstr "" #: cas_server/models.py:665 msgid "name of an attribute to send to the service, use * for all attributes" msgstr "" #: cas_server/models.py:672 cas_server/models.py:745 msgid "replace" msgstr "" #: cas_server/models.py:673 msgid "" "name under which the attribute will be show to the service. empty = default name of the " "attribut" msgstr "" #: cas_server/models.py:700 cas_server/models.py:731 msgid "attribute" msgstr "" #: cas_server/models.py:701 msgid "Name of the attribute which must verify pattern" msgstr "" #: cas_server/models.py:708 msgid "a regular expression" msgstr "" #: cas_server/models.py:732 msgid "Name of the attribute for which the value must be replace" msgstr "" #: cas_server/models.py:738 msgid "An regular expression maching whats need to be replaced" msgstr "" #: cas_server/models.py:746 msgid "replace expression, groups are capture by \\1, \\2 …" msgstr "" #: cas_server/templates/cas_server/base.html:43 #, python-format msgid "" "A new version of the application is available. This instance runs %(VERSION)s and the " "last version is %(LAST_VERSION)s. Please consider upgrading." msgstr "" "Uma nova versão da aplicação está disponível. Está instância usa a versão %(VERSION)s e " "a última versão é %(LAST_VERSION)s. Por favor, considere a atualização." #: cas_server/templates/cas_server/logged.html:4 msgid "" "

Log In Successful

You have successfully logged into the Central Authentication " "Service.
For security reasons, please Log Out and Exit your web browser when you are " "done accessing services that require authentication!" msgstr "" "

Log In realizado com sucesso

Você foi conectado com sucesso a Central de " "Autenticação de Serviços.
Por razões de segurança, faça o Log Out e saia do seu " "navegador quando você terminar de acessar os serviços que exigem auntenticação!" #: cas_server/templates/cas_server/logged.html:8 msgid "Log me out from all my sessions" msgstr "Desconecte-me de todas as sessões" #: cas_server/templates/cas_server/logged.html:14 msgid "Forget the identity provider" msgstr "Esquecer o provedor de identidade" #: cas_server/templates/cas_server/logged.html:18 msgid "Logout" msgstr "" #: cas_server/templates/cas_server/login.html:6 msgid "Please log in" msgstr "Por favor, faça log in" #: cas_server/templates/cas_server/login.html:14 msgid "Login" msgstr "" #: cas_server/templates/cas_server/warn.html:9 msgid "Connect to the service" msgstr "" #: cas_server/utils.py:744 #, python-format msgid "\"%(value)s\" is not a valid regular expression" msgstr "" #: cas_server/views.py:185 msgid "" "

Logout successful

You have successfully logged out from the Central " "Authentication Service. For security reasons, close your web browser." msgstr "" "

Logout realizado com sucesso

Você foi desconectado com sucesso da Central de " "Autenticação de Serviços. Por razões de segurança, feche seu navegador." #: cas_server/views.py:191 #, python-format msgid "" "

Logout successful

You have successfully logged out from %s sessions of the " "Central Authentication Service. For security reasons, close your web browser." msgstr "" "

Logout realizado com sucesso

Você foi desconectado com sucesso da %s sessão da " "Centralde Autenticação de Serviços. Por razões de segurança, feche seu navegador." #: cas_server/views.py:198 msgid "" "

Logout successful

You were already logged out from the Central Authentication " "Service. For security reasons, close your web browser." msgstr "" "

Logout realizado com sucesso

Você já está desconectado da Central de " "Autenticação de Serviços. Por razões de segurança, feche seu navegador." #: cas_server/views.py:378 #, python-format msgid "" "Invalid response from your identity provider CAS upon ticket %(ticket)s validation: " "%(error)r" msgstr "" "Resposta inválida do provedor de identidade CAS sobre o ticket %(ticket)svalidação: " "%(error)r" #: cas_server/views.py:500 msgid "Invalid login ticket, please try to log in again" msgstr "Ticket de login inválido, por favor tente novamente" #: cas_server/views.py:693 #, python-format msgid "Authentication has been required by service %(name)s (%(url)s)" msgstr "Autenticação requerida pelo serviço %(name)s (%(url)s)" #: cas_server/views.py:731 #, python-format msgid "Service %(url)s not allowed." msgstr "Serviço %(url)s não permitido" #: cas_server/views.py:738 msgid "Username not allowed" msgstr "Usuário não permitido" #: cas_server/views.py:745 msgid "User characteristics not allowed" msgstr "Características de usuário não permitida" #: cas_server/views.py:752 #, python-format msgid "The attribute %(field)s is needed to use that service" msgstr "O atributo %(field)s é necessário para usar o serviço" #: cas_server/views.py:842 #, python-format msgid "Authentication renewal required by service %(name)s (%(url)s)." msgstr "Renovação da autenticação requerida pelo serviço %(name)s (%(url)s)." #: cas_server/views.py:849 #, python-format msgid "Authentication required by service %(name)s (%(url)s)." msgstr "Autenticação requerida pelo serviço %(name)s (%(url)s)." #: cas_server/views.py:856 #, python-format msgid "Service %s not allowed" msgstr "Serviço %s não permitido" django-cas-server-1.1.0/cas_server/locale/pt_BR/django.mo0000644000175000017500000001223013203573210024677 0ustar valentinvalentin00000000000000) 7V>>(6g0]{< "95B5 x A  +  . A J S F9I:&O!v4`"E>h"8O@+U.*Yp     

Log In Successful

You have successfully logged into the Central Authentication Service.
For security reasons, please Log Out and Exit your web browser when you are done accessing services that require authentication!

Logout successful

You have successfully logged out from %s sessions of the Central Authentication Service. For security reasons, close your web browser.

Logout successful

You have successfully logged out from the Central Authentication Service. For security reasons, close your web browser.

Logout successful

You were already logged out from the Central Authentication Service. For security reasons, close your web browser.A new version of the application is available. This instance runs %(VERSION)s and the last version is %(LAST_VERSION)s. Please consider upgrading.Authentication has been required by service %(name)s (%(url)s)Authentication renewal required by service %(name)s (%(url)s).Authentication required by service %(name)s (%(url)s).Central Authentication ServiceForget the identity providerIdentity providerInvalid login ticket, please try to log in againInvalid response from your identity provider CAS upon ticket %(ticket)s validation: %(error)rLog me out from all my sessionsName for this identity provider displayed on the login page.Please log inRemember the identity providerService %(url)s not allowed.Service %s not allowedThe Central Authentication Service grants you access to most of our websites by authenticating only once, so you don't need to type your credentials again unless your session expires or you logout.The attribute %(field)s is needed to use that serviceThe credentials you provided cannot be determined to be authentic.User characteristics not allowedUser not found in the temporary database, please try to reconnectUsername not allowedWarn me before logging me into other sites.identity provideridentity providerspasswordusernameProject-Id-Version: Report-Msgid-Bugs-To: POT-Creation-Date: 2017-08-22 08:18-0300 PO-Revision-Date: 2017-08-29 18:09+0200 Language-Team: Roberto Morati Language: pt_BR MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n > 1); Last-Translator: Valentin Samir X-Generator: Poedit 1.8.11

Log In realizado com sucesso

Você foi conectado com sucesso a Central de Autenticação de Serviços.
Por razões de segurança, faça o Log Out e saia do seu navegador quando você terminar de acessar os serviços que exigem auntenticação!

Logout realizado com sucesso

Você foi desconectado com sucesso da %s sessão da Centralde Autenticação de Serviços. Por razões de segurança, feche seu navegador.

Logout realizado com sucesso

Você foi desconectado com sucesso da Central de Autenticação de Serviços. Por razões de segurança, feche seu navegador.

Logout realizado com sucesso

Você já está desconectado da Central de Autenticação de Serviços. Por razões de segurança, feche seu navegador.Uma nova versão da aplicação está disponível. Está instância usa a versão %(VERSION)s e a última versão é %(LAST_VERSION)s. Por favor, considere a atualização.Autenticação requerida pelo serviço %(name)s (%(url)s)Renovação da autenticação requerida pelo serviço %(name)s (%(url)s).Autenticação requerida pelo serviço %(name)s (%(url)s).Central de Autenticação de ServiçosEsquecer o provedor de identidadeProvedor de identidadeTicket de login inválido, por favor tente novamenteResposta inválida do provedor de identidade CAS sobre o ticket %(ticket)svalidação: %(error)rDesconecte-me de todas as sessõesNome para exibir o provedor de identidade na página de login.Por favor, faça log inRelembrar o provedor de identidadeServiço %(url)s não permitidoServiço %s não permitidoA Central de Autenticação de Serviços garante seu acesso à maioria dos nossos sitespor meio de uma única autenticação, então você não precisa digitar suas credenciaisnovamente, ao menos que sua sessão expire ou seu logout.O atributo %(field)s é necessário para usar o serviçoAs credenciais que você forneceu não podem ser determinadas como autênticas.Características de usuário não permitidaUsuário não encontrado na base de dados temporária, por favor, tente se reconectarUsuário não permitidoAvise-me antes de me registrar em outros sitesprovedor de identidadeprovedores de identidadesenhausuáriodjango-cas-server-1.1.0/cas_server/locale/nl/0000755000175000017500000000000013436457571022530 5ustar valentinvalentin00000000000000django-cas-server-1.1.0/cas_server/locale/nl/LC_MESSAGES/0000755000175000017500000000000013436457571024315 5ustar valentinvalentin00000000000000django-cas-server-1.1.0/cas_server/locale/nl/LC_MESSAGES/django.po0000644000175000017500000002742413203573210026105 0ustar valentinvalentin00000000000000# SOME DESCRIPTIVE TITLE. # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER # This file is distributed under the same license as the PACKAGE package. # FIRST AUTHOR , YEAR. # #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2017-03-29 12:24+0200\n" "PO-Revision-Date: 2017-03-29 13:06+0020\n" "Last-Translator: Joriks Kraaikamp \n" "Language-Team: LANGUAGE \n" "Language: \n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n" "X-Translated-Using: Mobetta 0.0.2\n" #: apps.py:25 templates/cas_server/base.html:7 #: templates/cas_server/base.html:26 msgid "Central Authentication Service" msgstr "Centrale Authenticatie Service" #: default_settings.py:201 msgid "" "The Central Authentication Service grants you access to most of our websites" " by authenticating only once, so you don't need to type your credentials " "again unless your session expires or you logout." msgstr "" "De Centrale Authenticatie Service geeft je toegang tot onze sites door 1 " "keer in te loggen, behalve als je sessie is verlopen of als je hebt " "uitgelogd." #: forms.py:85 msgid "Identity provider" msgstr "Identiteit provider" #: forms.py:89 forms.py:111 msgid "Warn me before logging me into other sites." msgstr "Waarschuw me voor het inloggen op andere websites." #: forms.py:93 msgid "Remember the identity provider" msgstr "Onthoudt deze identiteit provider" #: forms.py:104 models.py:638 msgid "username" msgstr "gebruikersnaam" #: forms.py:108 msgid "password" msgstr "wachtwoord" #: forms.py:131 msgid "The credentials you provided cannot be determined to be authentic." msgstr "De inloggegevens waren niet correct." #: forms.py:183 msgid "User not found in the temporary database, please try to reconnect" msgstr "" "De gebruiker is niet gevonden in de tijdelijke database, probeer het opnieuw" #: forms.py:197 msgid "service" msgstr "dienst" #: management/commands/cas_clean_federate.py:20 msgid "Clean old federated users" msgstr "Schoon oude gefedereerde gebruikers op" #: management/commands/cas_clean_sessions.py:22 msgid "Clean deleted sessions" msgstr "Schoon verwijderde sessies op" #: management/commands/cas_clean_tickets.py:22 msgid "Clean old tickets" msgstr "Schoon oude tickets op" #: models.py:71 msgid "identity provider" msgstr "identiteit provider" #: models.py:72 msgid "identity providers" msgstr "Identiteit providers" #: models.py:78 msgid "suffix" msgstr "achtervoegsel" #: models.py:80 msgid "" "Suffix append to backend CAS returned username: ``returned_username`` @ " "``suffix``." msgstr "" "Achtervoegsel toevoegen aan de gebruikersnaam door CAS backend: " "``teruggegeven_gebruikersnaam`` @ ``achtervoegsel``." #: models.py:87 msgid "server url" msgstr "server url" #: models.py:97 msgid "CAS protocol version" msgstr "CAS protocol versie" #: models.py:99 msgid "" "Version of the CAS protocol to use when sending requests the the backend " "CAS." msgstr "" "Versie van het CAS protocol wanneer we een request sturen van de CAS " "backend." #: models.py:106 msgid "verbose name" msgstr "uitgebreide naam" #: models.py:107 msgid "Name for this identity provider displayed on the login page." msgstr "Naam van de identiteit provider weergegeven op de login pagina." #: models.py:113 models.py:490 msgid "position" msgstr "positie" #: models.py:127 msgid "display" msgstr "weergave" #: models.py:128 msgid "Display the provider on the login page." msgstr "geef de provider weer op de login pagina." #: models.py:166 msgid "Federated user" msgstr "Gefedereerde gebruiker" #: models.py:167 msgid "Federated users" msgstr "Gefedereerde gebruikers" #: models.py:246 msgid "User attributes cache" msgstr "Gebruiker attributen cache" #: models.py:247 msgid "User attributes caches" msgstr "Gebruiker attributen caches" #: models.py:271 msgid "User" msgstr "Gebruiker" #: models.py:272 msgid "Users" msgstr "Gebruikers" #: models.py:364 #, python-format msgid "Error during service logout %s" msgstr "Een error is opgetreden tijdens het uitloggen %s" #: models.py:484 msgid "Service pattern" msgstr "Service patroon" #: models.py:485 msgid "Services patterns" msgstr "Service patronen" #: models.py:491 msgid "service patterns are sorted using the position attribute" msgstr "service patronen worden gesorteerd aan de hand van de positie." #: models.py:499 models.py:664 msgid "name" msgstr "naam" #: models.py:500 msgid "A name for the service" msgstr "Een naam voor de service" #: models.py:508 models.py:707 models.py:737 msgid "pattern" msgstr "patroon" #: models.py:510 msgid "" "A regular expression matching services. Will usually looks like " "'^https://some\\.server\\.com/path/.*$'.As it is a regular expression, " "special character must be escaped with a '\\'." msgstr "" "A reguliere expressie die de matched met de services. Ziet er vaak zou uit: " "'^https://some\\.server\\.com/path/.*$'. Omdat het een reguliere expressie " "is, moeten speciale characters worden geescaped met een '\\'." #: models.py:521 msgid "user field" msgstr "gebruiker veld" #: models.py:522 msgid "Name of the attribute to transmit as username, empty = login" msgstr "" "Naam van het attribuut om verstuurd te worden al gebruikersnaam, leeg = " "login" #: models.py:527 msgid "restrict username" msgstr "beperk gebruikersnamen" #: models.py:528 msgid "Limit username allowed to connect to the list provided bellow" msgstr "" "Beperk de gebruikersnamen die deze provider mogen gebruiken met de list " "hieronder." #: models.py:533 msgid "proxy" msgstr "proxy" #: models.py:534 msgid "Proxy tickets can be delivered to the service" msgstr "Proxy tickets kunnen worden aangeleverd aan de service" #: models.py:540 msgid "proxy callback" msgstr "proxy callback" #: models.py:541 msgid "can be used as a proxy callback to deliver PGT" msgstr "kan gebruikt worden als proxy callback voor geleverde PGT" #: models.py:548 msgid "single log out" msgstr "eenmalig afmelden" #: models.py:549 msgid "Enable SLO for the service" msgstr "Activeer eenmalig afmelden voor de service" #: models.py:557 msgid "single log out callback" msgstr "eenmalig afmelden callback" #: models.py:558 msgid "" "URL where the SLO request will be POST. empty = service url\n" "This is usefull for non HTTP proxied services." msgstr "" "De URL waar de 'eenmalige afmelding' request naar wordt gepost. empty = service url\n" "Dit is handig voor niet HTTP geproxide services." #: models.py:639 msgid "username allowed to connect to the service" msgstr "" "gebruikersnamen die zijn toegestaan om een connectie te maken via deze " "service." #: models.py:665 msgid "name of an attribute to send to the service, use * for all attributes" msgstr "" "naam van een attribuut dat naar de service verstuurd wordt. Gebruik * voor " "alle attributen." #: models.py:672 models.py:745 msgid "replace" msgstr "vervangen" #: models.py:673 msgid "" "name under which the attribute will be show to the service. empty = default " "name of the attribut" msgstr "" "onder deze naam wordt het attribuut naar de service verstuurd. leeg = " "attribuut naam" #: models.py:700 models.py:731 msgid "attribute" msgstr "attribuut" #: models.py:701 msgid "Name of the attribute which must verify pattern" msgstr "Naam van het attribuut dat het patroon moet valideren." #: models.py:708 msgid "a regular expression" msgstr "een reguliere expressie" #: models.py:732 msgid "Name of the attribute for which the value must be replace" msgstr "Naam van het attribuut waarvoor de waarde moet worden vervangen" #: models.py:738 msgid "An regular expression maching whats need to be replaced" msgstr "Een reguliere expressie dat matched wat vervangen moet worden" #: models.py:746 msgid "replace expression, groups are capture by \\1, \\2 …" msgstr "vervang expressie, groepen zijn vast te leggen door \\1. \\2 ..." #: templates/cas_server/base.html:43 #, python-format msgid "" "A new version of the application is available. This instance runs " "%(VERSION)s and the last version is %(LAST_VERSION)s. Please consider " "upgrading." msgstr "" "Een nieuwe versie van de applicatie is beschikbaar. Deze applicatie draait " "op versie %(VERSION)s en de nieuwste versie is %(LAST_VERSION)s. Denk na " "over het uitvoegen van een upgrade." #: templates/cas_server/logged.html:4 msgid "" "

Log In Successful

You have successfully logged into the Central " "Authentication Service.
For security reasons, please Log Out and Exit " "your web browser when you are done accessing services that require " "authentication!" msgstr "" "

Succesvol ingelogd.

Je bent succesvol ingelogd op het CAS.
Voor " "veiligheids-redenen, log uit en sluit de webbrowsers wanneer je klaar bent " "met de service!" #: templates/cas_server/logged.html:8 msgid "Log me out from all my sessions" msgstr "Meld me af bij alle sessies" #: templates/cas_server/logged.html:14 msgid "Forget the identity provider" msgstr "Vergeet de identiteits-provider" #: templates/cas_server/logged.html:18 msgid "Logout" msgstr "Afmelden" #: templates/cas_server/login.html:6 msgid "Please log in" msgstr "Log alstublieft in" #: templates/cas_server/login.html:14 msgid "Login" msgstr "Log in" #: templates/cas_server/warn.html:9 msgid "Connect to the service" msgstr "Verbind met de service" #: utils.py:744 #, python-format msgid "\"%(value)s\" is not a valid regular expression" msgstr "\"%(value)s\" is geen geldige reguliere expressie" #: views.py:185 msgid "" "

Logout successful

You have successfully logged out from the Central " "Authentication Service. For security reasons, close your web browser." msgstr "" "

Succesvol afgemeld.

Je bent succesvol afgemeld van de CAS.
Voor " "veiligheids-redenen, sluit de webbrowser!" #: views.py:191 #, python-format msgid "" "

Logout successful

You have successfully logged out from %s sessions " "of the Central Authentication Service. For security reasons, close your web " "browser." msgstr "" "

Succesvol afgemeld.

Je bent succesvol afgemeld van de %s " "sessie.
Voor veiligheids-redenen, sluit de webbrowser!" #: views.py:198 msgid "" "

Logout successful

You were already logged out from the Central " "Authentication Service. For security reasons, close your web browser." msgstr "" "

Succesvol afgemeld.

Je was al afgemeld van de CAS.
Voor " "veiligheids-redenen, sluit de webbrowser!" #: views.py:378 #, python-format msgid "" "Invalid response from your identity provider CAS upon ticket %(ticket)s " "validation: %(error)r" msgstr "" "Ongeldig response van de identiteit provider bij ticket %(ticket)s " "validatie: %(error)s" #: views.py:500 msgid "Invalid login ticket, please try to log in again" msgstr "Ongeldig login ticket. probeer het alstublieft opnieuw" #: views.py:693 #, python-format msgid "Authentication has been required by service %(name)s (%(url)s)" msgstr "service %(name)s (%(url)s) vraagt om authenticatie" #: views.py:731 #, python-format msgid "Service %(url)s not allowed." msgstr "Service %(url)s is niet toegestaan." #: views.py:738 msgid "Username not allowed" msgstr "Gebruikersnaam niet teogestaan" #: views.py:745 msgid "User characteristics not allowed" msgstr "Gebruikers karakteristieken niet toegestaan." #: views.py:752 #, python-format msgid "The attribute %(field)s is needed to use that service" msgstr "Het attribuut %(field)s is nodig voor die service" #: views.py:842 #, python-format msgid "Authentication renewal required by service %(name)s (%(url)s)." msgstr "" "Service %(name)s (%(url)s) vraagt om vernieuwing van de authenticatie." #: views.py:849 #, python-format msgid "Authentication required by service %(name)s (%(url)s)." msgstr "Service %(name)s (%(url)s) vraagt om authenticatie" #: views.py:856 #, python-format msgid "Service %s not allowed" msgstr "Service %s niet toegestaan" django-cas-server-1.1.0/cas_server/locale/nl/LC_MESSAGES/django.mo0000644000175000017500000002233013203573210026071 0ustar valentinvalentin00000000000000T\\-]p0G7 > > 6C z      ' 0 K j y   0 ] =G    < 9 <) /f -   %5SG5aBjEJ` wAM+Co .E`54 8'`o * /w{Num@=Q2F2 <Po&)* 07h6WRZ??MY66!(#Jnt1$   , L!N! m!Mx!2!! "9"U"^"r""["T" =#H#P#X#^# m#>w## ##>#$0$ K$Y$h$Ow$$"%(value)s" is not a valid regular expression

Log In Successful

You have successfully logged into the Central Authentication Service.
For security reasons, please Log Out and Exit your web browser when you are done accessing services that require authentication!

Logout successful

You have successfully logged out from %s sessions of the Central Authentication Service. For security reasons, close your web browser.

Logout successful

You have successfully logged out from the Central Authentication Service. For security reasons, close your web browser.

Logout successful

You were already logged out from the Central Authentication Service. For security reasons, close your web browser.A name for the serviceA new version of the application is available. This instance runs %(VERSION)s and the last version is %(LAST_VERSION)s. Please consider upgrading.A regular expression matching services. Will usually looks like '^https://some\.server\.com/path/.*$'.As it is a regular expression, special character must be escaped with a '\'.An regular expression maching whats need to be replacedAuthentication has been required by service %(name)s (%(url)s)Authentication renewal required by service %(name)s (%(url)s).Authentication required by service %(name)s (%(url)s).CAS protocol versionCentral Authentication ServiceClean deleted sessionsClean old federated usersClean old ticketsConnect to the serviceDisplay the provider on the login page.Enable SLO for the serviceError during service logout %sFederated userFederated usersForget the identity providerIdentity providerInvalid login ticket, please try to log in againInvalid response from your identity provider CAS upon ticket %(ticket)s validation: %(error)rLimit username allowed to connect to the list provided bellowLog me out from all my sessionsLoginLogoutName for this identity provider displayed on the login page.Name of the attribute for which the value must be replaceName of the attribute to transmit as username, empty = loginName of the attribute which must verify patternPlease log inProxy tickets can be delivered to the serviceRemember the identity providerService %(url)s not allowed.Service %s not allowedService patternServices patternsSuffix append to backend CAS returned username: ``returned_username`` @ ``suffix``.The Central Authentication Service grants you access to most of our websites by authenticating only once, so you don't need to type your credentials again unless your session expires or you logout.The attribute %(field)s is needed to use that serviceThe credentials you provided cannot be determined to be authentic.URL where the SLO request will be POST. empty = service url This is usefull for non HTTP proxied services.UserUser attributes cacheUser attributes cachesUser characteristics not allowedUser not found in the temporary database, please try to reconnectUsername not allowedUsersVersion of the CAS protocol to use when sending requests the the backend CAS.Warn me before logging me into other sites.a regular expressionattributecan be used as a proxy callback to deliver PGTdisplayidentity provideridentity providersnamename of an attribute to send to the service, use * for all attributesname under which the attribute will be show to the service. empty = default name of the attributpasswordpatternpositionproxyproxy callbackreplacereplace expression, groups are capture by \1, \2 …restrict usernameserver urlserviceservice patterns are sorted using the position attributesingle log outsingle log out callbacksuffixuser fieldusernameusername allowed to connect to the serviceverbose nameProject-Id-Version: PACKAGE VERSION Report-Msgid-Bugs-To: POT-Creation-Date: 2017-03-29 12:24+0200 PO-Revision-Date: 2017-03-29 13:06+0020 Last-Translator: Joriks Kraaikamp Language-Team: LANGUAGE Language: MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Plural-Forms: nplurals=2; plural=(n != 1); X-Translated-Using: Mobetta 0.0.2 "%(value)s" is geen geldige reguliere expressie

Succesvol ingelogd.

Je bent succesvol ingelogd op het CAS.
Voor veiligheids-redenen, log uit en sluit de webbrowsers wanneer je klaar bent met de service!

Succesvol afgemeld.

Je bent succesvol afgemeld van de %s sessie.
Voor veiligheids-redenen, sluit de webbrowser!

Succesvol afgemeld.

Je bent succesvol afgemeld van de CAS.
Voor veiligheids-redenen, sluit de webbrowser!

Succesvol afgemeld.

Je was al afgemeld van de CAS.
Voor veiligheids-redenen, sluit de webbrowser!Een naam voor de serviceEen nieuwe versie van de applicatie is beschikbaar. Deze applicatie draait op versie %(VERSION)s en de nieuwste versie is %(LAST_VERSION)s. Denk na over het uitvoegen van een upgrade.A reguliere expressie die de matched met de services. Ziet er vaak zou uit: '^https://some\.server\.com/path/.*$'. Omdat het een reguliere expressie is, moeten speciale characters worden geescaped met een '\'.Een reguliere expressie dat matched wat vervangen moet wordenservice %(name)s (%(url)s) vraagt om authenticatieService %(name)s (%(url)s) vraagt om vernieuwing van de authenticatie.Service %(name)s (%(url)s) vraagt om authenticatieCAS protocol versieCentrale Authenticatie ServiceSchoon verwijderde sessies opSchoon oude gefedereerde gebruikers opSchoon oude tickets opVerbind met de servicegeef de provider weer op de login pagina.Activeer eenmalig afmelden voor de serviceEen error is opgetreden tijdens het uitloggen %sGefedereerde gebruikerGefedereerde gebruikersVergeet de identiteits-providerIdentiteit providerOngeldig login ticket. probeer het alstublieft opnieuwOngeldig response van de identiteit provider bij ticket %(ticket)s validatie: %(error)sBeperk de gebruikersnamen die deze provider mogen gebruiken met de list hieronder.Meld me af bij alle sessiesLog inAfmeldenNaam van de identiteit provider weergegeven op de login pagina.Naam van het attribuut waarvoor de waarde moet worden vervangenNaam van het attribuut om verstuurd te worden al gebruikersnaam, leeg = loginNaam van het attribuut dat het patroon moet valideren.Log alstublieft inProxy tickets kunnen worden aangeleverd aan de serviceOnthoudt deze identiteit providerService %(url)s is niet toegestaan.Service %s niet toegestaanService patroonService patronenAchtervoegsel toevoegen aan de gebruikersnaam door CAS backend: ``teruggegeven_gebruikersnaam`` @ ``achtervoegsel``.De Centrale Authenticatie Service geeft je toegang tot onze sites door 1 keer in te loggen, behalve als je sessie is verlopen of als je hebt uitgelogd.Het attribuut %(field)s is nodig voor die serviceDe inloggegevens waren niet correct.De URL waar de 'eenmalige afmelding' request naar wordt gepost. empty = service url Dit is handig voor niet HTTP geproxide services.GebruikerGebruiker attributen cacheGebruiker attributen cachesGebruikers karakteristieken niet toegestaan.De gebruiker is niet gevonden in de tijdelijke database, probeer het opnieuwGebruikersnaam niet teogestaanGebruikersVersie van het CAS protocol wanneer we een request sturen van de CAS backend.Waarschuw me voor het inloggen op andere websites.een reguliere expressieattribuutkan gebruikt worden als proxy callback voor geleverde PGTweergaveidentiteit providerIdentiteit providersnaamnaam van een attribuut dat naar de service verstuurd wordt. Gebruik * voor alle attributen.onder deze naam wordt het attribuut naar de service verstuurd. leeg = attribuut naamwachtwoordpatroonpositieproxyproxy callbackvervangenvervang expressie, groepen zijn vast te leggen door \1. \2 ...beperk gebruikersnamenserver urldienstservice patronen worden gesorteerd aan de hand van de positie.eenmalig afmeldeneenmalig afmelden callbackachtervoegselgebruiker veldgebruikersnaamgebruikersnamen die zijn toegestaan om een connectie te maken via deze service.uitgebreide naamdjango-cas-server-1.1.0/cas_server/static/0000755000175000017500000000000013436457571022147 5ustar valentinvalentin00000000000000django-cas-server-1.1.0/cas_server/static/cas_server/0000755000175000017500000000000013436457571024303 5ustar valentinvalentin00000000000000django-cas-server-1.1.0/cas_server/static/cas_server/logo.png0000644000175000017500000002705212757022243025744 0ustar valentinvalentin00000000000000PNG  IHDRsBIT|d pHYsZ;OtEXtSoftwarewww.inkscape.org< IDATxy|3ͻƎ=!{=[ [JC%B4rr.GoBl(6/($$!Y&[9VgFƖ\|Gfyhcp\ړcoʲ|_EE}Op82V8uyPk;}YjUdZ&򅌱%&l"]XEaas.KF87ce}9+dY1XCӡv!k֭[gk0N-2wݟ=!Iқ1vk׮ ~LD7(qgljqB[n3LBǃ9]X^sn%")޶$IZ 2"26` R"ZZSSlMyy6v yow[jU Dt! '6mUS-q Dt>DDwy<@\: ȹrJl+ E~p8nبvmSL&vSò11ج]D՘!{$7MEV^=SOcEcegg|r2IZO"ψkצ!eeecf ct:bŊT NS49s> j̐euepB;y`=@ɜ]lk;!c-_i26\].W=nuOOo|I5;yD8}_xm?9 l%,rcnI&XC1=vknr:Jc{¬Y Hv Zrf]z\y%>ТW&%\.y"'A^_9irg4rAuuF32)QSS〺bu%TTT|^TTt.{ծaL9wfn{[%lZ 5nwΦMz***~AR(ZoxGASӶ`"vkTN"+x\`QQ*#@HW*e+ng\. %^ί2$Ĵ6ͮA``cNN/++ozo onP:GD"\^|BDtK4r9a췳@ "zlѢPիW/Š&*v3~xѻ~VxD8eYFf2Z0rjrP崧Bm: Ddd[U1Fq\2cl zGap%R""Cz- f+B#p, }N,T8~uuu)?tB'lD$i2iCl˪ו/_.5G&gb|I=k;ÅGy tN5O "c}a@Sr5dY^r3=F5kcm1v)YFhb8ŵm$i/ LVװU^^^/IR"ǝۼ^29vcpPHqJ v{ʖMR:h``$"g ge)DDcm4bt{bŊTIzZ B`2ߵ^7 ؛f9↯{Q^IL}|mJȲ|P]BpۍAs Ḙ1V!\5sBZ%33S`08ߎ@pܟȲlviPe1pt:obtKtAP[ bC,d2My"QADqt:#2u `;p:M&S |S(z!w@D~S|¦M%X9Aj#މYDc蠻I$DDO@ۜ1D/I3bKMMMN Z [9WAlPy!sCCY|H E+b̥a#ٰaC=` BԊBߎW5DFn1y,ˑ?u3X`}Ocn[V'9T:iQ'|0mܸ )"~E cQM3zu,Mw֯_oS2)eE8gķA@ʰD4gA \OJQ='"83^6 {<>(pR 1 Dg)mV) (5Na眫,o c<#1`0sԈeY>LjvDʩ+ԲAE hf+1SVV6*ch'[;Oz j'fAP[+Ϻ9v &c(DRm6UN`mJȲ|)<Zm 2VN,XqCeӶ8R[[{9"gG_+|U˷OxjA1&1vPh!#F1ŋIu gR ~[:k\} 'RKU]á%}$z?TI(0P7z([d: rI8wJT A,CIssPΓw;kYVV Bw0QJzQ *O"`=_TTs]6-BLF@鼃1v~\2b BDDm B>$$pj1XpbYA`D$PMݞˏ>h'EH(<h'lŷf@HłX4lذiڭO$PFHdr-N(X<vFDUN$0LCٽDQQ֡'t'F$t:/.E"VVVV6ӟ"t:oXreNED7(~AH4 Hq0BD?3$=*“zkzق FDw@$)'oZNXcy۠ŕ*АP(t$Ic/kbZHGor}' D$I: C-|x#F/$#IvgzZt4T6wDtNEEZŖ&G4 rJ6<J*++/&y!O 0pcXNܲ6ms'Mc$=Ggwg (=;OZƿhS c2M)[sKdɐ3-[gK6b CM)4쵙n(L}gFAf6ϔ)f [E5#gL>w9$?(FU2A(6l^oLrgH2`$3B!bZn,~ݢQ XD:c^￾ll鱺 t}/QJy7 L?޿;Df#/1ftUBWO(EM{m[78 ʊe/38$ԈƂ_4}]pqi9ŨQ[4d롆UPٷީF~UwhHғ ?N*ѣ"7\Nbcٳسo:g,>RgD{V324x}OcMk5z/ip̞j/V`_29?gᄜ\5/y.ltaM4]}cMƜ[:$'ͬ{oxvcJ]=C8E+;f {&edZKR)!DZ{KfhJRD78Gɔhn9c Tx’f`29Fsq@^tռѴ_ޡҰҁL~aۉۗMKP.ޥj7BΚkrњ\5&7jfIn@ܞza[5ZCwUgXM!1(0fknZ2,j.]5F*Y_ں{~_<јGϚ݊:F(ƀk2-ͲisΞ04hZם1fdJ Lh&e }nAŌ.--I1 }cOMádNQ 0XLVlh3c+ )qW4FThBnf4~MR.W@(guMeYVۢy15R-i# c,i hX)E3 t#mowaQ(fQczT`?vT7:W-k JGgܘ v+ڄuX[G%Y{w 2,(׳GGc^mZ]<)/fco>+dX`+N`ڈ,kka+VGkz 0~d)fIJ9v醆 5]`'sam$)fAlvQγ[Em D[M?\;gr~9EMGۮ-t:գ j;2ĭVC\,[ՍS2R;mmCY-sFӊsR4OuՍLϙ6'J bQ4=Z[ϚEQY8g,zf;:mq}ۑp7VEcfqvk x,/]Iյunk^m7$>"fk*ZF IDAT5dYm':wM(HO5gΙ?.Gs򝁾&&#Bvm@C@+;kzTr*dJ~IA/V>[jR^dzFfh Ye0~HG0;EϺ'^\y躸t$#q &m.;qDQp(rnqP'd +ux4lX6]k̔H:d2J.19'(l䜱]6e\l%3\ %i˒yȇm[M#4ty W4{{uOsÙcf22#>\1gL dݣs#رX剕+#@'6%gL\<1\ 1W)pM!b9,@({}sz@"]wGn9wn#g`W+.`ZAm'rxw@36$+̉y*,,Ns_9OVC $^b9edF֘4KUcnwqiW?a-[jjh[Σ]Op}F~֚L3PwOmKwwMrs|Ue}."Q7(5{=`d霱co<{`]{gSgtvVQȰL6ThOQ)"̭>Qìdϯ?w(o9cz^qïxQvQv;dc^Fdv&D6 VsINmZqVTr/Սe2ES %!Mfl']1uOHJ]Nl[l۠&dvq ɯѽowlyXߙ>"&E66?mPj]dg>Ru!5K܍!C6FLt\"#V./ eǑ&# O4mEr1?oU2,xC," hz}usWOw 2mv]o֗@ UCخkLU0te ~0! =W7w\g<`h-f~qخP5֚>Ż:IqIݭ_ w~!˱ Pywu`hݟ+ޔn~9*zzahVQdv{Fmܱ{іH32 5 ZSEgptO[>?}qgumEd XDC[W-ǎcu=m2Ğzc؛iʺ sF~V~iVǎ #! "V gCXkˏeqz=x5 au!cZ46uwo{ݯN\昂Kg@_ֵOի_ZY,F ӊ35[.ȴ1 x#'LIX:0)SK2VeW&gcbl` 7׽DU bvǜ 5:'5-ޞ/::>uMxx4nW-q^nw)zy#zh'eQ:ڞ9qDFZ~f[uBǚۏ4aouHsւy˷;%LR4s׫70]QljQfʘT[fKXl&A􅂝@'l ֵw}~O?pC`%9)yynE VmA]`'C.mG;|@f {/_5u`xoM_nĨ֗`H[DNb!Qjc?krBM:7H^J抏I?L?vHplvIENDB`django-cas-server-1.1.0/cas_server/static/cas_server/functions.js0000644000175000017500000000211412757406463026646 0ustar valentinvalentin00000000000000function createCookie(name, value, days){ var expires; var date; if(days){ date = new Date(); date.setTime(date.getTime()+(days*24*60*60*1000)); expires = "; expires="+date.toGMTString(); } else{ expires = ""; } document.cookie = name + "=" + value + expires + "; path=/"; } function readCookie(name){ var nameEQ = name + "="; var ca = document.cookie.split(";"); for(var i=0;i < ca.length;i++) { var c = ca[i]; while (c.charAt(0) === " "){ c = c.substring(1,c.length); } if (c.indexOf(nameEQ) === 0){ return c.substring(nameEQ.length,c.length); } } return null; } function eraseCookie(name) { createCookie(name,"",-1); } function discard_and_remember(id, cookie_name, token, days=10*365){ jQuery(function( $ ){ $(id).click(function( e ){ e.preventDefault(); createCookie(cookie_name, token, days); }); if(readCookie(cookie_name) === token){ $(id).parent().hide(); } }); } django-cas-server-1.1.0/cas_server/static/cas_server/styles.css0000644000175000017500000000245712757022243026335 0ustar valentinvalentin00000000000000html, body { height: 100%; } body { padding-top: 40px; padding-bottom: 0; background-color: #eee; } .form-signin .form-signin-heading, .form-signin .checkbox { margin-bottom: 10px; } .form-signin .checkbox { font-weight: normal; } .form-signin .form-control { position: relative; height: auto; -webkit-box-sizing: border-box; -moz-box-sizing: border-box; box-sizing: border-box; padding: 10px; font-size: 16px; } .form-signin .form-control:focus { z-index: 2; } .form-signin input[type="text"] { margin-bottom: -1px; border-bottom-right-radius: 0; border-bottom-left-radius: 0; } .form-signin input[type="password"] { margin-bottom: 10px; border-top-left-radius: 0; border-top-right-radius: 0; } #app-name { text-align: center; } #app-name img { width:110px; } /* Wrapper for page content to push down footer */ #wrap { min-height: 100%; height: auto; height: 100%; /* Negative indent footer by it's height */ margin: 0 auto -40px; } #footer { height: 40px; text-align: center; } #footer p { padding-top: 10px; } @media screen and (max-width: 680px) { #app-name { margin: 0; } #app-name img { display: block; margin: auto; } body { padding-top: 0; padding-bottom: 0; } } django-cas-server-1.1.0/cas_server/static/cas_server/favicon.ico0000644000175000017500000001603412632536713026421 0ustar valentinvalentin00000000000000PNG  IHDRWSM )iCCPiccxڕgP} mқT) HYz^EY"bCDi Q)+X X,"*(GL{'?zw93" ~.06|G$/3 Q@%:&K \ JJ  @n3/f_>L~ {T|ABlL.?-Vɏagس>؄W@+pHKOowF,{ӿ3]xY2( LEP-c0+'poP<C( ZB7\kp}g )x!"t J:#qB<?$@T$ Gv"%H9R4 -OrF ,'PDP \ @ףqh{*=vл(*Dߠ `T)czs0,c[bkڱ^l{ 93c8=2p[qI\7}x]% f|'~?H XM9JH$l&:W ÄIH!HXD&"^!HTɘL # HVeiD#-h&rKG"/Q)kJ%REiܠSST՗@NޢNP?$h:4Z8-vvNkat}/~I!/&-MVKdD(YT]+A4OR=91XVZ bcb q#qoRV3D 'hBc%&C1v27SLSLd0O3&A$,rc%XgYX_R1R{ڥFc;GȰeddt<!;'ǔ˝{*o?&?(࢐Pp]aNhXxYqVdTtE5[e'yeyeW,!%M@Ujjj꼚ZZSu:G=^FnnMiM7<6q-VVCm6G;I}TT'^V.k{Xwx~ŪUhz\l6 }~~[50 M Iic35~y=ߙĘ1yl02mg܌on6kfa^g>ar|8[x {m->[Y ,Zagdj5FsM̚5*֑ BMQmmK;Uhfi67{Оoi`#X8$TY9ιyeUW~177[ۼ~GKOOguk|Ե}OϾ_ZW~F~~ jf-; C B M #5-sZwpTixQs !yå#7GGF,GzG6F.DEExxo+gccccccgW%8$$KtMO\LN:ܑBJH*ڟ6^.̰817g"3{LA`0K+kWDMvms⹩t64w|3n3os_r-- [Q[n+6eI;~)0,(/3xgoB].ڊDEcv!ᇡ=TV]|İdWzG~\w̬>¾}?Y.^W>y@WoWT:$VW\_3Zk_Q'_np#vGKM8QXWMAM9[eKH=!rۖ/kvkt_:̆oqwxۑk|G G?yK϶Nj=|!W_;fK/_>M-WWJ-33gg^zM{v ·O[L>-,qi̧9^Y&.W}J.P cHRMz&u0`:pQ<bKGD̿ pHYsHHFk>tIME 211'IDATxU]^&:9wvC@!Bn6}zӳ47_Tr^jU`wcG%uKcb\q*Ւ׿hթ}b_RQTh\jU'C9QZ}иFjT:@C5DlA-ᙚ,Cj,tAmӐYh\K-{j=)]6I<.e2ȥ~CqsJfbi54 ƭʢ;jNa jXi MY+DRqsj#iKpLh\B+WvUWP`%wIMn"51W;{&bh\-cS9('v.v#p(q');՚s 0 ]6z"2zrG@Jj==fhGޤ.hq=64n5i TiRODb Ri}0ǰM)UzdV gkLdz&gиYJGG61#v MRF tfftQBL $c1[6ZOj4f?N v7p!2Ho $2Tzd_Z\i\3!3Pqc_ʤIh0 MDDAlzLz"2t]Yi\"Mί f sUhGe׸M+B6/>{"-$nt ̉as@g=IJ{B7TM K G}^'YWC0i|TZu*՚kzRbsи*jumqj&pج6RK;neKmyL6sJK`fdиJ:8kkit[N7tdr6Ӎ_B Phyڱ[jS 颍uچZqpnMn.ڈ.^7|tV7$J㚀gYՎ8>A\@[v¶M& }M ~?Eu殴ڙ_+M)$Uؓu&@|ʙe:`T7sV$3zhC҄X[ަɯXXiL5'1r%mzϕfqy۔2Ƹf`?Tu&'G=j͠~4c7HmfPgC7Y>*㠊ƕzP+ؿ9l^wsCЙ1lEΩ̹J3IΙ⏅ٔ\J3>j힊Xao@NWq+uԙWeDȉиj.S.ڊ#҇q,GvYN毪J Th32@m&F><ۢ9ZL<{Μ"95RiFߤgOͶҧkw`od$Wo7&54n˽Sd[7{*o}נɀ~\i-_,q7'+}͸7\L>tFe=s4:hWXi-mQڕ>_N& M o6׌|So R⠍kWl,!4Ԛ Mo5IGhW||1 ؁&[7O)rZ[hJ8mgW=+ݳf`־q@QX"^^B3 DsOB zz ?>^4?Ac{r A1gT%8jp59]1ظZLݳuZ!2Yً: ow^$*}1כ))J.hnKH/3iv =r .NjpT7̰i75T*aͤdF5o*mS;ROt`^yQm^qsneLJ`y[=Xg׸33.ٻJzvC5QٞȜ9v3~u^ts-.w̞9?9C踹5E e\̣ cRk L3i糜/GϡF:hSC6:h/dsZ}4G+MGb0To5ON='od2`:s{s4grzmKJS陝wMHթ1|'~g|g ?lc7 HPkf?|ZCh5)?K14/S%4 4yZqqsN*54ҀwF:v3t\dxsY C#hC[hd4Җ.` J#jk14߲Љ3FҚ}Ϙ6sh5Ο^iL>czFw<+4:n:GN44`zF;b)"4rH~<HZruh|\_i &fмƆNL'l^j=DhR2H$jL`|?mPC##S(ߠa& S0ph 7xh$?ICF$5DT#Fw2XAF aR54L DE 0"4 `Dh#BF0"4z4qԞLlxF0"4 `Dh#BF0"4 `DhKL :BcFh!4:nCn B3y~ǸƆ@Z݄wƂ@ouC'R$?Z3v 'nFH؁ Ic6a `Dh#BF0"4 `Dh#BF0"4 `Dh#BF0"4 `Dh#BF0"4 `Dh#BF0"4 `Dh#BF0"4 `݀ImGhTc'4H!vN: U 8iO ciKhж#4Hﴈ݆Ov~+$ͷZ'Sm~n #WQUVhJV7F7y pjTkjuw7 :}秃%tEXtdate:create2015-11-20T15:50:49+01:00PQ%tEXtdate:modify2015-11-20T15:50:49+01:00~ tEXtps:HiResBoundingBox410x411+0-1whtEXtps:LevelAdobe-3.0 EPSF-3.0 pIENDB`django-cas-server-1.1.0/cas_server/static/cas_server/logo.svg0000644000175000017500000001752412757022243025762 0ustar valentinvalentin00000000000000 image/svg+xml CAS django-cas-server-1.1.0/cas_server/static/cas_server/cas.js0000644000175000017500000000252512757022243025400 0ustar valentinvalentin00000000000000function cas_login(cas_server_login, service, login_service, callback){ var url = cas_server_login + "?service=" + encodeURIComponent(service); $.ajax({ type: "GET", url, beforeSend(request) { request.setRequestHeader("X-AJAX", "1"); }, xhrFields: { withCredentials: true }, success(data, textStatus, request){ if(data.status === "success"){ $.ajax({ type: "GET", url: data.url, xhrFields: { withCredentials: true }, success: callback, error(request, textStatus, errorThrown) {}, }); } else { if(data.detail === "login required"){ window.location.href = cas_server_login + "?service=" + encodeURIComponent(login_service); } else { alert("error: " + data.messages[1].message); } } }, error(request, textStatus, errorThrown) {}, }); } function cas_logout(cas_server_logout){ $.ajax({ type: "GET", url: cas_server_logout, beforeSend(request) { request.setRequestHeader("X-AJAX", "1"); }, xhrFields: { withCredentials: true }, error(request, textStatus, errorThrown) {}, success(data, textStatus, request){ if(data.status === "error"){ alert("error: " + data.messages[1].message); } }, }); } django-cas-server-1.1.0/cas_server/utils.py0000644000175000017500000006357213416345433022376 0ustar valentinvalentin00000000000000# -*- coding: utf-8 -*- # 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 General Public License version 3 for # more details. # # You should have received a copy of the GNU General Public License version 3 # along with this program; if not, write to the Free Software Foundation, Inc., 51 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # (c) 2015-2016 Valentin Samir """Some util function for the app""" from .default_settings import settings from django.http import HttpResponseRedirect, HttpResponse from django.contrib import messages from django.contrib.messages import constants as DEFAULT_MESSAGE_LEVELS from django.core.serializers.json import DjangoJSONEncoder from django.utils import timezone from django.core.exceptions import ValidationError from django.utils.translation import ugettext_lazy as _ try: from django.urls import reverse except ImportError: from django.core.urlresolvers import reverse import re import random import string import json import hashlib import crypt import base64 import six import requests import time import logging import binascii from importlib import import_module from datetime import datetime, timedelta from six.moves.urllib.parse import urlparse, urlunparse, parse_qsl, urlencode from . import VERSION #: logger facility logger = logging.getLogger(__name__) def json_encode(obj): """Encode a python object to json""" try: return json_encode.encoder.encode(obj) except AttributeError: json_encode.encoder = DjangoJSONEncoder(default=six.text_type) return json_encode(obj) def context(params): """ Function that add somes variable to the context before template rendering :param dict params: The context dictionary used to render templates. :return: The ``params`` dictionary with the key ``settings`` set to :obj:`django.conf.settings`. :rtype: dict """ params["settings"] = settings params["message_levels"] = DEFAULT_MESSAGE_LEVELS if settings.CAS_NEW_VERSION_HTML_WARNING: LAST_VERSION = last_version() params["VERSION"] = VERSION params["LAST_VERSION"] = LAST_VERSION if LAST_VERSION is not None: params["upgrade_available"] = decode_version(VERSION) < decode_version(LAST_VERSION) else: params["upgrade_available"] = False if settings.CAS_INFO_MESSAGES_ORDER: params["CAS_INFO_RENDER"] = [] for msg_name in settings.CAS_INFO_MESSAGES_ORDER: if msg_name in settings.CAS_INFO_MESSAGES: if not isinstance(settings.CAS_INFO_MESSAGES[msg_name], dict): continue msg = settings.CAS_INFO_MESSAGES[msg_name].copy() if "message" in msg: msg["name"] = msg_name # use info as default infox type msg["type"] = msg.get("type", "info") # make box discardable by default msg["discardable"] = msg.get("discardable", True) msg_hash = ( six.text_type(msg["message"]).encode("utf-8") + msg["type"].encode("utf-8") ) # hash depend of the rendering language msg["hash"] = hashlib.md5(msg_hash).hexdigest() params["CAS_INFO_RENDER"].append(msg) return params def json_response(request, data): """ Wrapper dumping `data` to a json and sending it to the user with an HttpResponse :param django.http.HttpRequest request: The request object used to generate this response. :param dict data: The python dictionnary to return as a json :return: The content of ``data`` serialized in json :rtype: django.http.HttpResponse """ data["messages"] = [] for msg in messages.get_messages(request): data["messages"].append({'message': msg.message, 'level': msg.level_tag}) return HttpResponse(json.dumps(data), content_type="application/json") def import_attr(path): """ transform a python dotted path to the attr :param path: A dotted path to a python object or a python object :type path: :obj:`unicode` or :obj:`str` or anything :return: The python object pointed by the dotted path or the python object unchanged """ # if we got a str, decode it to unicode (normally it should only contain ascii) if isinstance(path, six.binary_type): path = path.decode("utf-8") # if path is not an unicode, return it unchanged (may be it is already the attribute to import) if not isinstance(path, six.text_type): return path if u"." not in path: ValueError("%r should be of the form `module.attr` and we just got `attr`" % path) module, attr = path.rsplit(u'.', 1) try: return getattr(import_module(module), attr) except ImportError: raise ImportError("Module %r not found" % module) except AttributeError: raise AttributeError("Module %r has not attribut %r" % (module, attr)) def redirect_params(url_name, params=None): """ Redirect to ``url_name`` with ``params`` as querystring :param unicode url_name: a URL pattern name :param params: Some parameter to append to the reversed URL :type params: :obj:`dict` or :obj:`NoneType` :return: A redirection to the URL with name ``url_name`` with ``params`` as querystring. :rtype: django.http.HttpResponseRedirect """ url = reverse(url_name) params = urlencode(params if params else {}) return HttpResponseRedirect(url + "?%s" % params) def reverse_params(url_name, params=None, **kwargs): """ compute the reverse url of ``url_name`` and add to it parameters from ``params`` as querystring :param unicode url_name: a URL pattern name :param params: Some parameter to append to the reversed URL :type params: :obj:`dict` or :obj:`NoneType` :param **kwargs: additional parameters needed to compure the reverse URL :return: The computed reverse URL of ``url_name`` with possible querystring from ``params`` :rtype: unicode """ url = reverse(url_name, **kwargs) params = urlencode(params if params else {}) if params: return u"%s?%s" % (url, params) else: return url def copy_params(get_or_post_params, ignore=None): """ copy a :class:`django.http.QueryDict` in a :obj:`dict` ignoring keys in the set ``ignore`` :param django.http.QueryDict get_or_post_params: A GET or POST :class:`QueryDict` :param set ignore: An optinal set of keys to ignore during the copy :return: A copy of get_or_post_params :rtype: dict """ if ignore is None: ignore = set() params = {} for key in get_or_post_params: if key not in ignore and get_or_post_params[key]: params[key] = get_or_post_params[key] return params def set_cookie(response, key, value, max_age): """ Set the cookie ``key`` on ``response`` with value ``value`` valid for ``max_age`` secondes :param django.http.HttpResponse response: a django response where to set the cookie :param unicode key: the cookie key :param unicode value: the cookie value :param int max_age: the maximum validity age of the cookie """ expires = datetime.strftime( datetime.utcnow() + timedelta(seconds=max_age), "%a, %d-%b-%Y %H:%M:%S GMT" ) response.set_cookie( key, value, max_age=max_age, expires=expires, domain=settings.SESSION_COOKIE_DOMAIN, secure=settings.SESSION_COOKIE_SECURE or None ) def get_current_url(request, ignore_params=None): """ Giving a django request, return the current http url, possibly ignoring some GET parameters :param django.http.HttpRequest request: The current request object. :param set ignore_params: An optional set of GET parameters to ignore :return: The URL of the current page, possibly omitting some parameters from ``ignore_params`` in the querystring. :rtype: unicode """ if ignore_params is None: ignore_params = set() protocol = u'https' if request.is_secure() else u"http" service_url = u"%s://%s%s" % (protocol, request.get_host(), request.path) if request.GET: params = copy_params(request.GET, ignore_params) if params: service_url += u"?%s" % urlencode(params) return service_url def update_url(url, params): """ update parameters using ``params`` in the ``url`` query string :param url: An URL possibily with a querystring :type url: :obj:`unicode` or :obj:`str` :param dict params: A dictionary of parameters for updating the url querystring :return: The URL with an updated querystring :rtype: unicode """ if not isinstance(url, bytes): url = url.encode('utf-8') for key, value in list(params.items()): if not isinstance(key, bytes): del params[key] key = key.encode('utf-8') if not isinstance(value, bytes): value = value.encode('utf-8') params[key] = value url_parts = list(urlparse(url)) query = dict(parse_qsl(url_parts[4])) query.update(params) # make the params order deterministic query = list(query.items()) query.sort() url_query = urlencode(query) if not isinstance(url_query, bytes): # pragma: no cover in python3 urlencode return an unicode url_query = url_query.encode("utf-8") url_parts[4] = url_query return urlunparse(url_parts).decode('utf-8') def unpack_nested_exception(error): """ If exception are stacked, return the first one :param error: A python exception with possible exception embeded within :return: A python exception with no exception embeded within """ i = 0 while True: if error.args[i:]: if isinstance(error.args[i], Exception): error = error.args[i] i = 0 else: i += 1 else: break return error def _gen_ticket(prefix=None, lg=settings.CAS_TICKET_LEN): """ Generate a ticket with prefix ``prefix`` and length ``lg`` :param unicode prefix: An optional prefix (probably ST, PT, PGT or PGTIOU) :param int lg: The length of the generated ticket (with the prefix) :return: A randomlly generated ticket of length ``lg`` :rtype: unicode """ random_part = u''.join( random.choice( string.ascii_letters + string.digits ) for _ in range(lg - len(prefix or "") - 1) ) if prefix is not None: return u'%s-%s' % (prefix, random_part) else: return random_part def gen_lt(): """ Generate a Login Ticket :return: A ticket with prefix ``settings.CAS_LOGIN_TICKET_PREFIX`` and length ``settings.CAS_LT_LEN`` :rtype: unicode """ return _gen_ticket(settings.CAS_LOGIN_TICKET_PREFIX, settings.CAS_LT_LEN) def gen_st(): """ Generate a Service Ticket :return: A ticket with prefix ``settings.CAS_SERVICE_TICKET_PREFIX`` and length ``settings.CAS_ST_LEN`` :rtype: unicode """ return _gen_ticket(settings.CAS_SERVICE_TICKET_PREFIX, settings.CAS_ST_LEN) def gen_pt(): """ Generate a Proxy Ticket :return: A ticket with prefix ``settings.CAS_PROXY_TICKET_PREFIX`` and length ``settings.CAS_PT_LEN`` :rtype: unicode """ return _gen_ticket(settings.CAS_PROXY_TICKET_PREFIX, settings.CAS_PT_LEN) def gen_pgt(): """ Generate a Proxy Granting Ticket :return: A ticket with prefix ``settings.CAS_PROXY_GRANTING_TICKET_PREFIX`` and length ``settings.CAS_PGT_LEN`` :rtype: unicode """ return _gen_ticket(settings.CAS_PROXY_GRANTING_TICKET_PREFIX, settings.CAS_PGT_LEN) def gen_pgtiou(): """ Generate a Proxy Granting Ticket IOU :return: A ticket with prefix ``settings.CAS_PROXY_GRANTING_TICKET_IOU_PREFIX`` and length ``settings.CAS_PGTIOU_LEN`` :rtype: unicode """ return _gen_ticket(settings.CAS_PROXY_GRANTING_TICKET_IOU_PREFIX, settings.CAS_PGTIOU_LEN) def gen_saml_id(): """ Generate an saml id :return: A random id of length ``settings.CAS_TICKET_LEN`` :rtype: unicode """ return _gen_ticket() def get_tuple(nuplet, index, default=None): """ :param tuple nuplet: A tuple :param int index: An index :param default: An optional default value :return: ``nuplet[index]`` if defined, else ``default`` (possibly ``None``) """ if nuplet is None: return default try: return nuplet[index] except IndexError: return default def crypt_salt_is_valid(salt): """ Validate a salt as crypt salt :param str salt: a password salt :return: ``True`` if ``salt`` is a valid crypt salt on this system, ``False`` otherwise :rtype: bool """ if len(salt) < 2: return False else: if salt[0] == '$': if salt[1] == '$': return False else: if '$' not in salt[1:]: return False else: hashed = crypt.crypt("", salt) if not hashed or '$' not in hashed[1:]: return False else: return True else: return True class LdapHashUserPassword(object): """ Class to deal with hashed password as defined at https://tools.ietf.org/id/draft-stroeder-hashed-userpassword-values-01.html """ #: valide schemes that require a salt schemes_salt = {b"{SMD5}", b"{SSHA}", b"{SSHA256}", b"{SSHA384}", b"{SSHA512}", b"{CRYPT}"} #: valide sschemes that require no slat schemes_nosalt = {b"{MD5}", b"{SHA}", b"{SHA256}", b"{SHA384}", b"{SHA512}"} #: map beetween scheme and hash function _schemes_to_hash = { b"{SMD5}": hashlib.md5, b"{MD5}": hashlib.md5, b"{SSHA}": hashlib.sha1, b"{SHA}": hashlib.sha1, b"{SSHA256}": hashlib.sha256, b"{SHA256}": hashlib.sha256, b"{SSHA384}": hashlib.sha384, b"{SHA384}": hashlib.sha384, b"{SSHA512}": hashlib.sha512, b"{SHA512}": hashlib.sha512 } #: map between scheme and hash length _schemes_to_len = { b"{SMD5}": 16, b"{SSHA}": 20, b"{SSHA256}": 32, b"{SSHA384}": 48, b"{SSHA512}": 64, } class BadScheme(ValueError): """ Error raised then the hash scheme is not in :attr:`LdapHashUserPassword.schemes_salt` + :attr:`LdapHashUserPassword.schemes_nosalt` """ pass class BadHash(ValueError): """Error raised then the hash is too short""" pass class BadSalt(ValueError): """Error raised then, with the scheme ``{CRYPT}``, the salt is invalid""" pass @classmethod def _raise_bad_scheme(cls, scheme, valid, msg): """ Raise :attr:`BadScheme` error for ``scheme``, possible valid scheme are in ``valid``, the error message is ``msg`` :param bytes scheme: A bad scheme :param list valid: A list a valid scheme :param str msg: The error template message :raises LdapHashUserPassword.BadScheme: always """ valid_schemes = [s.decode() for s in valid] valid_schemes.sort() raise cls.BadScheme(msg % (scheme, u", ".join(valid_schemes))) @classmethod def _test_scheme(cls, scheme): """ Test if a scheme is valide or raise BadScheme :param bytes scheme: A scheme :raises BadScheme: if ``scheme`` is not a valid scheme """ if scheme not in cls.schemes_salt and scheme not in cls.schemes_nosalt: cls._raise_bad_scheme( scheme, cls.schemes_salt | cls.schemes_nosalt, "The scheme %r is not valid. Valide schemes are %s." ) @classmethod def _test_scheme_salt(cls, scheme): """ Test if the scheme need a salt or raise BadScheme :param bytes scheme: A scheme :raises BadScheme: if ``scheme` require no salt """ if scheme not in cls.schemes_salt: cls._raise_bad_scheme( scheme, cls.schemes_salt, "The scheme %r is only valid without a salt. Valide schemes with salt are %s." ) @classmethod def _test_scheme_nosalt(cls, scheme): """ Test if the scheme need no salt or raise BadScheme :param bytes scheme: A scheme :raises BadScheme: if ``scheme` require a salt """ if scheme not in cls.schemes_nosalt: cls._raise_bad_scheme( scheme, cls.schemes_nosalt, "The scheme %r is only valid with a salt. Valide schemes without salt are %s." ) @classmethod def hash(cls, scheme, password, salt=None, charset="utf8"): """ Hash ``password`` with ``scheme`` using ``salt``. This three variable beeing encoded in ``charset``. :param bytes scheme: A valid scheme :param bytes password: A byte string to hash using ``scheme`` :param bytes salt: An optional salt to use if ``scheme`` requires any :param str charset: The encoding of ``scheme``, ``password`` and ``salt`` :return: The hashed password encoded with ``charset`` :rtype: bytes """ scheme = scheme.upper() cls._test_scheme(scheme) if salt is None or salt == b"": salt = b"" cls._test_scheme_nosalt(scheme) else: cls._test_scheme_salt(scheme) try: return scheme + base64.b64encode( cls._schemes_to_hash[scheme](password + salt).digest() + salt ) except KeyError: if six.PY3: password = password.decode(charset) salt = salt.decode(charset) if not crypt_salt_is_valid(salt): raise cls.BadSalt("System crypt implementation do not support the salt %r" % salt) hashed_password = crypt.crypt(password, salt) if six.PY3: hashed_password = hashed_password.encode(charset) return scheme + hashed_password @classmethod def get_scheme(cls, hashed_passord): """ Return the scheme of ``hashed_passord`` or raise :attr:`BadHash` :param bytes hashed_passord: A hashed password :return: The scheme used by the hashed password :rtype: bytes :raises BadHash: if no valid scheme is found within ``hashed_passord`` """ if not hashed_passord[0] == b'{'[0] or b'}' not in hashed_passord: raise cls.BadHash("%r should start with the scheme enclosed with { }" % hashed_passord) scheme = hashed_passord.split(b'}', 1)[0] scheme = scheme.upper() + b"}" return scheme @classmethod def get_salt(cls, hashed_passord): """ Return the salt of ``hashed_passord`` possibly empty :param bytes hashed_passord: A hashed password :return: The salt used by the hashed password (empty if no salt is used) :rtype: bytes :raises BadHash: if no valid scheme is found within ``hashed_passord`` or if the hashed password is too short for the scheme found. """ scheme = cls.get_scheme(hashed_passord) cls._test_scheme(scheme) if scheme in cls.schemes_nosalt: return b"" elif scheme == b'{CRYPT}': return b'$'.join(hashed_passord.split(b'$', 3)[:-1])[len(scheme):] else: try: hashed_passord = base64.b64decode(hashed_passord[len(scheme):]) except (TypeError, binascii.Error) as error: raise cls.BadHash("Bad base64: %s" % error) if len(hashed_passord) < cls._schemes_to_len[scheme]: raise cls.BadHash("Hash too short for the scheme %s" % scheme) return hashed_passord[cls._schemes_to_len[scheme]:] def check_password(method, password, hashed_password, charset): """ Check that ``password`` match `hashed_password` using ``method``, assuming the encoding is ``charset``. :param str method: on of ``"crypt"``, ``"ldap"``, ``"hex_md5"``, ``"hex_sha1"``, ``"hex_sha224"``, ``"hex_sha256"``, ``"hex_sha384"``, ``"hex_sha512"``, ``"plain"`` :param password: The user inputed password :type password: :obj:`str` or :obj:`unicode` :param hashed_password: The hashed password as stored in the database :type hashed_password: :obj:`str` or :obj:`unicode` :param str charset: The used char encoding (also used internally, so it must be valid for the charset used by ``password`` when it was initially ) :return: True if ``password`` match ``hashed_password`` using ``method``, ``False`` otherwise :rtype: bool """ if not isinstance(password, six.binary_type): password = password.encode(charset) if not isinstance(hashed_password, six.binary_type): hashed_password = hashed_password.encode(charset) if method == "plain": return password == hashed_password elif method == "crypt": if hashed_password.startswith(b'$'): salt = b'$'.join(hashed_password.split(b'$', 3)[:-1]) elif hashed_password.startswith(b'_'): # pragma: no cover old BSD format not supported salt = hashed_password[:9] else: salt = hashed_password[:2] if six.PY3: password = password.decode(charset) salt = salt.decode(charset) hashed_password = hashed_password.decode(charset) if not crypt_salt_is_valid(salt): raise ValueError("System crypt implementation do not support the salt %r" % salt) crypted_password = crypt.crypt(password, salt) return crypted_password == hashed_password elif method == "ldap": scheme = LdapHashUserPassword.get_scheme(hashed_password) salt = LdapHashUserPassword.get_salt(hashed_password) return LdapHashUserPassword.hash(scheme, password, salt, charset=charset) == hashed_password elif ( method.startswith("hex_") and method[4:] in {"md5", "sha1", "sha224", "sha256", "sha384", "sha512"} ): return getattr( hashlib, method[4:] )(password).hexdigest().encode("ascii") == hashed_password.lower() else: raise ValueError("Unknown password method check %r" % method) def decode_version(version): """ decode a version string following version semantic http://semver.org/ input a tuple of int. It will work as long as we do not use pre release versions. :param unicode version: A dotted version :return: A tuple a int :rtype: tuple """ return tuple(int(sub_version) for sub_version in version.split('.')) def last_version(): """ Fetch the last version from pypi and return it. On successful fetch from pypi, the response is cached 24h, on error, it is cached 10 min. :return: the last django-cas-server version :rtype: unicode """ try: last_update, version, success = last_version._cache except AttributeError: last_update = 0 version = None success = False cache_delta = 24 * 3600 if success else 600 if (time.time() - last_update) < cache_delta: return version else: try: req = requests.get(settings.CAS_NEW_VERSION_JSON_URL) data = json.loads(req.text) version = data["info"]["version"] last_version._cache = (time.time(), version, True) return version except ( KeyError, ValueError, requests.exceptions.RequestException ) as error: # pragma: no cover (should not happen unless pypi is not available) logger.error( "Unable to fetch %s: %s" % (settings.CAS_NEW_VERSION_JSON_URL, error) ) last_version._cache = (time.time(), version, False) def dictfetchall(cursor): "Return all rows from a django cursor as a dict" columns = [col[0] for col in cursor.description] return [ dict(zip(columns, row)) for row in cursor.fetchall() ] def logout_request(ticket): """ Forge a SLO logout request :param unicode ticket: A ticket value :return: A SLO XML body request :rtype: unicode """ return u""" %(ticket)s """ % { 'id': gen_saml_id(), 'datetime': timezone.now().isoformat(), 'ticket': ticket } def regexpr_validator(value): """ Test that ``value`` is a valid regular expression :param unicode value: A regular expression to test :raises ValidationError: if ``value`` is not a valid regular expression """ try: re.compile(value) except re.error: raise ValidationError( _('"%(value)s" is not a valid regular expression'), params={'value': value} ) django-cas-server-1.1.0/cas_server/__init__.py0000644000175000017500000000126613436457341022771 0ustar valentinvalentin00000000000000# 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 General Public License version 3 for # more details. # # You should have received a copy of the GNU General Public License version 3 # along with this program; if not, write to the Free Software Foundation, Inc., 51 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # (c) 2015-2016 Valentin Samir """A django CAS server application""" #: version of the application VERSION = '1.1.0' #: path the the application configuration class default_app_config = 'cas_server.apps.CasAppConfig' django-cas-server-1.1.0/cas_server/forms.py0000644000175000017500000001765213436457341022366 0ustar valentinvalentin00000000000000# 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 General Public License version 3 for # more details. # # You should have received a copy of the GNU General Public License version 3 # along with this program; if not, write to the Free Software Foundation, Inc., 51 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # (c) 2015-2016 Valentin Samir """forms for the app""" from .default_settings import settings from django import forms from django.forms import widgets from django.utils.translation import ugettext_lazy as _ import cas_server.utils as utils import cas_server.models as models class BootsrapForm(forms.Form): """ Bases: :class:`django.forms.Form` Form base class to use boostrap then rendering the form fields """ def __init__(self, *args, **kwargs): super(BootsrapForm, self).__init__(*args, **kwargs) for field in self.fields.values(): # Only tweak the field if it will be displayed if not isinstance(field.widget, widgets.HiddenInput): attrs = {} if ( isinstance(field.widget, (widgets.Input, widgets.Select, widgets.Textarea)) and not isinstance(field.widget, (widgets.CheckboxInput,)) ): attrs['class'] = "form-control" if isinstance(field.widget, (widgets.Input, widgets.Textarea)) and field.label: attrs["placeholder"] = field.label if field.required: attrs["required"] = "required" field.widget.attrs.update(attrs) class BaseLogin(BootsrapForm): """ Bases: :class:`BootsrapForm` Base form with all field possibly hidden on the login pages """ #: The service url for which the user want a ticket service = forms.CharField(widget=forms.HiddenInput(), required=False) #: A valid LoginTicket to prevent POST replay lt = forms.CharField(widget=forms.HiddenInput(), required=False) #: Is the service asking the authentication renewal ? renew = forms.BooleanField(widget=forms.HiddenInput(), required=False) #: Url to redirect to if the authentication fail (user not authenticated or bad service) gateway = forms.CharField(widget=forms.HiddenInput(), required=False) method = forms.CharField(widget=forms.HiddenInput(), required=False) class WarnForm(BaseLogin): """ Bases: :class:`BaseLogin` Form used on warn page before emiting a ticket """ #: ``True`` if the user has been warned of the ticket emission warned = forms.BooleanField(widget=forms.HiddenInput(), required=False) class FederateSelect(BaseLogin): """ Bases: :class:`BaseLogin` Form used on the login page when ``settings.CAS_FEDERATE`` is ``True`` allowing the user to choose an identity provider. """ #: The providers the user can choose to be used as authentication backend provider = forms.ModelChoiceField( queryset=models.FederatedIendityProvider.objects.filter(display=True).order_by( "pos", "verbose_name", "suffix" ), to_field_name="suffix", label=_('Identity provider'), ) #: A checkbox to ask to be warn before emiting a ticket for another service warn = forms.BooleanField( label=_('Warn me before logging me into other sites.'), required=False ) #: A checkbox to remember the user choices of :attr:`provider` remember = forms.BooleanField(label=_('Remember the identity provider'), required=False) class UserCredential(BaseLogin): """ Bases: :class:`BaseLogin` Form used on the login page to retrive user credentials """ #: The user username username = forms.CharField( label=_('username'), widget=forms.TextInput(attrs={'autofocus': 'autofocus'}) ) #: The user password password = forms.CharField(label=_('password'), widget=forms.PasswordInput) #: A checkbox to ask to be warn before emiting a ticket for another service warn = forms.BooleanField( label=_('Warn me before logging me into other sites.'), required=False ) def clean(self): """ Validate that the submited :attr:`username` and :attr:`password` are valid :raises django.forms.ValidationError: if the :attr:`username` and :attr:`password` are not valid. :return: The cleaned POST data :rtype: dict """ cleaned_data = super(UserCredential, self).clean() if "username" in cleaned_data and "password" in cleaned_data: auth = utils.import_attr(settings.CAS_AUTH_CLASS)(cleaned_data["username"]) if auth.test_password(cleaned_data["password"]): cleaned_data["username"] = auth.username else: raise forms.ValidationError( _(u"The credentials you provided cannot be determined to be authentic.") ) return cleaned_data class FederateUserCredential(UserCredential): """ Bases: :class:`UserCredential` Form used on a auto submited page for linking the views :class:`FederateAuth` and :class:`LoginView`. On successful authentication on a provider, in the view :class:`FederateAuth` a :class:`FederatedUser` is created by :meth:`cas_server.federate.CASFederateValidateUser.verify_ticket` and the user is redirected to :class:`LoginView`. This form is then automatically filled with infos matching the created :class:`FederatedUser` using the ``ticket`` as one time password and submited using javascript. If javascript is not enabled, a connect button is displayed. This stub authentication form, allow to implement the federated mode with very few modificatons to the :class:`LoginView` view. """ def __init__(self, *args, **kwargs): super(FederateUserCredential, self).__init__(*args, **kwargs) # All fields are hidden and auto filled by the /login view logic for name, field in self.fields.items(): field.widget = forms.HiddenInput() self[name].display = False def clean(self): """ Validate that the submited :attr:`username` and :attr:`password` are valid using the :class:`CASFederateAuth` auth class. :raises django.forms.ValidationError: if the :attr:`username` and :attr:`password` do not correspond to a :class:`FederatedUser`. :return: The cleaned POST data :rtype: dict """ cleaned_data = super(FederateUserCredential, self).clean() try: user = models.FederatedUser.get_from_federated_username(cleaned_data["username"]) user.ticket = "" user.save() # should not happed as if the FederatedUser do not exists, super should # raise before a ValidationError("bad user") except models.FederatedUser.DoesNotExist: # pragma: no cover (should not happend) raise forms.ValidationError( _(u"User not found in the temporary database, please try to reconnect") ) return cleaned_data class TicketForm(forms.ModelForm): """ Bases: :class:`django.forms.ModelForm` Form for Tickets in the admin interface """ class Meta: model = models.Ticket exclude = [] service = forms.CharField(label=_('service'), widget=forms.TextInput) django-cas-server-1.1.0/cas_server/views.py0000644000175000017500000017012413416345433022363 0ustar valentinvalentin00000000000000# -*- coding: utf-8 -*- # 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 General Public License version 3 for # more details. # # You should have received a copy of the GNU General Public License version 3 # along with this program; if not, write to the Free Software Foundation, Inc., 51 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # (c) 2015-2016 Valentin Samir """views for the app""" from .default_settings import settings, SessionStore from django.shortcuts import render, redirect from django.http import HttpResponse, HttpResponseRedirect from django.contrib import messages from django.utils.decorators import method_decorator from django.utils.translation import ugettext as _ from django.utils import timezone from django.views.decorators.csrf import csrf_exempt from django.middleware.csrf import CsrfViewMiddleware from django.views.generic import View from django.utils.encoding import python_2_unicode_compatible from django.utils.safestring import mark_safe try: from django.urls import reverse except ImportError: from django.core.urlresolvers import reverse import re import logging import pprint import requests from lxml import etree from datetime import timedelta import cas_server.utils as utils import cas_server.forms as forms import cas_server.models as models from .utils import json_response from .models import Ticket, ServiceTicket, ProxyTicket, ProxyGrantingTicket from .models import ServicePattern, FederatedIendityProvider, FederatedUser from .federate import CASFederateValidateUser logger = logging.getLogger(__name__) class LogoutMixin(object): """destroy CAS session utils""" def logout(self, all_session=False): """ effectively destroy a CAS session :param boolean all_session: If ``True`` destroy all the user sessions, otherwise destroy the current user session. :return: The number of destroyed sessions :rtype: int """ # initialize the counter of the number of destroyed sesisons session_nb = 0 # save the current user username before flushing the session username = self.request.session.get("username") if username: if all_session: logger.info("Logging out user %s from all sessions." % username) else: logger.info("Logging out user %s." % username) users = [] # try to get the user from the current session try: users.append( models.User.objects.get( username=username, session_key=self.request.session.session_key ) ) except models.User.DoesNotExist: # if user not found in database, flush the session anyway self.request.session.flush() # If all_session is set, search all of the user sessions if all_session: users.extend( models.User.objects.filter( username=username ).exclude( session_key=self.request.session.session_key ) ) # Iterate over all user sessions that have to be logged out for user in users: # get the user session session = SessionStore(session_key=user.session_key) # flush the session session.flush() # send SLO requests user.logout(self.request) # delete the user user.delete() # increment the destroyed session counter session_nb += 1 if username: logger.info("User %s logged out" % username) return session_nb class CsrfExemptView(View): """base class for csrf exempt class views""" @method_decorator(csrf_exempt) # csrf is disabled for allowing SLO requests reception def dispatch(self, request, *args, **kwargs): """ dispatch different http request to the methods of the same name :param django.http.HttpRequest request: The current request object """ return super(CsrfExemptView, self).dispatch(request, *args, **kwargs) class LogoutView(View, LogoutMixin): """destroy CAS session (logout) view""" #: current :class:`django.http.HttpRequest` object request = None #: service GET parameter service = None #: url GET paramet url = None #: ``True`` if the HTTP_X_AJAX http header is sent and ``settings.CAS_ENABLE_AJAX_AUTH`` #: is ``True``, ``False`` otherwise. ajax = None def init_get(self, request): """ Initialize the :class:`LogoutView` attributes on GET request :param django.http.HttpRequest request: The current request object """ self.request = request self.service = request.GET.get('service') self.url = request.GET.get('url') self.ajax = settings.CAS_ENABLE_AJAX_AUTH and 'HTTP_X_AJAX' in request.META def get(self, request, *args, **kwargs): """ method called on GET request on this view :param django.http.HttpRequest request: The current request object """ logger.info("logout requested") # initialize the class attributes self.init_get(request) # if CAS federation mode is enable, bakup the provider before flushing the sessions if settings.CAS_FEDERATE: try: user = FederatedUser.get_from_federated_username( self.request.session.get("username") ) auth = CASFederateValidateUser(user.provider, service_url="") except FederatedUser.DoesNotExist: auth = None session_nb = self.logout(self.request.GET.get("all")) # if CAS federation mode is enable, redirect to user CAS logout page, appending the # current querystring if settings.CAS_FEDERATE: if auth is not None: params = utils.copy_params(request.GET, ignore={"forget_provider"}) url = auth.get_logout_url() response = HttpResponseRedirect(utils.update_url(url, params)) if request.GET.get("forget_provider"): response.delete_cookie("remember_provider") return response # if service is set, redirect to service after logout if self.service: list(messages.get_messages(request)) # clean messages before leaving the django app return HttpResponseRedirect(self.service) # if service is not set but url is set, redirect to url after logout elif self.url: list(messages.get_messages(request)) # clean messages before leaving the django app return HttpResponseRedirect(self.url) else: # build logout message depending of the number of sessions the user logs out if session_nb == 1: logout_msg = mark_safe(_( "

Logout successful

" "You have successfully logged out from the Central Authentication Service. " "For security reasons, close your web browser." )) elif session_nb > 1: logout_msg = mark_safe(_( "

Logout successful

" "You have successfully logged out from %d sessions of the Central " "Authentication Service. " "For security reasons, close your web browser." ) % session_nb) else: logout_msg = mark_safe(_( "

Logout successful

" "You were already logged out from the Central Authentication Service. " "For security reasons, close your web browser." )) # depending of settings, redirect to the login page with a logout message or display # the logout page. The default is to display tge logout page. if settings.CAS_REDIRECT_TO_LOGIN_AFTER_LOGOUT: messages.add_message(request, messages.SUCCESS, logout_msg) if self.ajax: url = reverse("cas_server:login") data = { 'status': 'success', 'detail': 'logout', 'url': url, 'session_nb': session_nb } return json_response(request, data) else: return redirect("cas_server:login") else: if self.ajax: data = {'status': 'success', 'detail': 'logout', 'session_nb': session_nb} return json_response(request, data) else: return render( request, settings.CAS_LOGOUT_TEMPLATE, utils.context({'logout_msg': logout_msg}) ) class FederateAuth(CsrfExemptView): """ view to authenticated user against a backend CAS then CAS_FEDERATE is True csrf is disabled for allowing SLO requests reception. """ #: current URL used as service URL by the CAS client service_url = None def get_cas_client(self, request, provider, renew=False): """ return a CAS client object matching provider :param django.http.HttpRequest request: The current request object :param cas_server.models.FederatedIendityProvider provider: the user identity provider :return: The user CAS client object :rtype: :class:`federate.CASFederateValidateUser ` """ # compute the current url, ignoring ticket dans provider GET parameters service_url = utils.get_current_url(request, {"ticket", "provider"}) self.service_url = service_url return CASFederateValidateUser(provider, service_url, renew=renew) def post(self, request, provider=None): """ method called on POST request :param django.http.HttpRequest request: The current request object :param unicode provider: Optional parameter. The user provider suffix. """ # if settings.CAS_FEDERATE is not True redirect to the login page if not settings.CAS_FEDERATE: logger.warning("CAS_FEDERATE is False, set it to True to use federation") return redirect("cas_server:login") # POST with a provider suffix, this is probably an SLO request. csrf is disabled for # allowing SLO requests reception try: provider = FederatedIendityProvider.objects.get(suffix=provider) auth = self.get_cas_client(request, provider) try: auth.clean_sessions(request.POST['logoutRequest']) except (KeyError, AttributeError): pass return HttpResponse("ok") # else, a User is trying to log in using an identity provider except FederatedIendityProvider.DoesNotExist: # Manually checking for csrf to protect the code below reason = CsrfViewMiddleware().process_view(request, None, (), {}) if reason is not None: # pragma: no cover (csrf checks are disabled during tests) return reason # Failed the test, stop here. form = forms.FederateSelect(request.POST) if form.is_valid(): params = utils.copy_params( request.POST, ignore={"provider", "csrfmiddlewaretoken", "ticket", "lt"} ) if params.get("renew") == "False": del params["renew"] url = utils.reverse_params( "cas_server:federateAuth", kwargs=dict(provider=form.cleaned_data["provider"].suffix), params=params ) return HttpResponseRedirect(url) else: return redirect("cas_server:login") def get(self, request, provider=None): """ method called on GET request :param django.http.HttpRequestself. request: The current request object :param unicode provider: Optional parameter. The user provider suffix. """ # if settings.CAS_FEDERATE is not True redirect to the login page if not settings.CAS_FEDERATE: logger.warning("CAS_FEDERATE is False, set it to True to use federation") return redirect("cas_server:login") renew = bool(request.GET.get('renew') and request.GET['renew'] != "False") # Is the user is already authenticated, no need to request authentication to the user # identity provider. if self.request.session.get("authenticated") and not renew: logger.warning("User already authenticated, dropping federated authentication request") return redirect("cas_server:login") try: # get the identity provider from its suffix provider = FederatedIendityProvider.objects.get(suffix=provider) # get a CAS client for the user identity provider auth = self.get_cas_client(request, provider, renew) # if no ticket submited, redirect to the identity provider CAS login page if 'ticket' not in request.GET: logger.info("Trying to authenticate %s again" % auth.provider.server_url) return HttpResponseRedirect(auth.get_login_url()) else: ticket = request.GET['ticket'] try: # if the ticket validation succeed if auth.verify_ticket(ticket): logger.info( "Got a valid ticket for %s from %s" % ( auth.username, auth.provider.server_url ) ) params = utils.copy_params(request.GET, ignore={"ticket", "remember"}) request.session["federate_username"] = auth.federated_username request.session["federate_ticket"] = ticket auth.register_slo( auth.federated_username, request.session.session_key, ticket ) # redirect to the the login page for the user to become authenticated # thanks to the `federate_username` and `federate_ticket` session parameters url = utils.reverse_params("cas_server:login", params) response = HttpResponseRedirect(url) # If the user has checked "remember my identity provider" store it in a # cookie if request.GET.get("remember"): max_age = settings.CAS_FEDERATE_REMEMBER_TIMEOUT utils.set_cookie( response, "remember_provider", provider.suffix, max_age ) return response # else redirect to the identity provider CAS login page else: logger.info( ( "Got an invalid ticket %s from %s for service %s. " "Retrying authentication" ) % ( ticket, auth.provider.server_url, self.service_url ) ) return HttpResponseRedirect(auth.get_login_url()) # both xml.etree.ElementTree and lxml.etree exceptions inherit from SyntaxError except SyntaxError as error: messages.add_message( request, messages.ERROR, _( u"Invalid response from your identity provider CAS upon " u"ticket %(ticket)s validation: %(error)r" ) % {'ticket': ticket, 'error': error} ) response = redirect("cas_server:login") response.delete_cookie("remember_provider") return response except FederatedIendityProvider.DoesNotExist: logger.warning("Identity provider suffix %s not found" % provider) # if the identity provider is not found, redirect to the login page return redirect("cas_server:login") class LoginView(View, LogoutMixin): """credential requestor / acceptor""" # pylint: disable=too-many-instance-attributes # Nine is reasonable in this case. #: The current :class:`models.User` object user = None #: The form to display to the user form = None #: current :class:`django.http.HttpRequest` object request = None #: service GET/POST parameter service = None #: ``True`` if renew GET/POST parameter is present and not "False" renew = None #: the warn GET/POST parameter warn = None #: the gateway GET/POST parameter gateway = None #: the method GET/POST parameter method = None #: ``True`` if the HTTP_X_AJAX http header is sent and ``settings.CAS_ENABLE_AJAX_AUTH`` #: is ``True``, ``False`` otherwise. ajax = None #: ``True`` if the user has just authenticated renewed = False #: ``True`` if renew GET/POST parameter is present and not "False" warned = False #: The :class:`FederateAuth` transmited username (only used if ``settings.CAS_FEDERATE`` #: is ``True``) username = None #: The :class:`FederateAuth` transmited ticket (only used if ``settings.CAS_FEDERATE`` is #: ``True``) ticket = None INVALID_LOGIN_TICKET = 1 USER_LOGIN_OK = 2 USER_LOGIN_FAILURE = 3 USER_ALREADY_LOGGED = 4 USER_AUTHENTICATED = 5 USER_NOT_AUTHENTICATED = 6 def init_post(self, request): """ Initialize POST received parameters :param django.http.HttpRequest request: The current request object """ self.request = request self.service = request.POST.get('service') self.renew = bool(request.POST.get('renew') and request.POST['renew'] != "False") self.gateway = request.POST.get('gateway') self.method = request.POST.get('method') self.ajax = settings.CAS_ENABLE_AJAX_AUTH and 'HTTP_X_AJAX' in request.META if request.POST.get('warned') and request.POST['warned'] != "False": self.warned = True self.warn = request.POST.get('warn') if settings.CAS_FEDERATE: self.username = request.POST.get('username') # in federated mode, the valdated indentity provider CAS ticket is used as password self.ticket = request.POST.get('password') def gen_lt(self): """Generate a new LoginTicket and add it to the list of valid LT for the user""" self.request.session['lt'] = self.request.session.get('lt', []) + [utils.gen_lt()] if len(self.request.session['lt']) > 100: self.request.session['lt'] = self.request.session['lt'][-100:] def check_lt(self): """ Check is the POSTed LoginTicket is valid, if yes invalide it :return: ``True`` if the LoginTicket is valid, ``False`` otherwise :rtype: bool """ # save LT for later check lt_valid = self.request.session.get('lt', []) lt_send = self.request.POST.get('lt') # generate a new LT (by posting the LT has been consumed) self.gen_lt() # check if send LT is valid if lt_send not in lt_valid: return False else: self.request.session['lt'].remove(lt_send) # we need to redo the affectation for django to detect that the list has changed # and for its new value to be store in the session self.request.session['lt'] = self.request.session['lt'] return True def post(self, request, *args, **kwargs): """ method called on POST request on this view :param django.http.HttpRequest request: The current request object """ # initialize class parameters self.init_post(request) # process the POST request ret = self.process_post() if ret == self.INVALID_LOGIN_TICKET: messages.add_message( self.request, messages.ERROR, _(u"Invalid login ticket, please try to log in again") ) elif ret == self.USER_LOGIN_OK: # On successful login, update the :class:`models.User` ``date`` # attribute by saving it. (``auto_now=True``) self.user = models.User.objects.get_or_create( username=self.request.session['username'], session_key=self.request.session.session_key )[0] self.user.last_login = timezone.now() self.user.save() elif ret == self.USER_LOGIN_FAILURE: # bad user login if settings.CAS_FEDERATE: self.ticket = None self.username = None self.init_form() # preserve valid LoginTickets from session flush lt = self.request.session.get('lt', []) # On login failure, flush the session self.logout() # restore valid LoginTickets self.request.session['lt'] = lt elif ret == self.USER_ALREADY_LOGGED: pass else: # pragma: no cover (should no happen) raise EnvironmentError("invalid output for LoginView.process_post") # call the GET/POST common part response = self.common() if self.warn: utils.set_cookie( response, "warn", "on", 10 * 365 * 24 * 3600 ) else: response.delete_cookie("warn") return response def process_post(self): """ Analyse the POST request: * check that the LoginTicket is valid * check that the user sumited credentials are valid :return: * :attr:`INVALID_LOGIN_TICKET` if the POSTed LoginTicket is not valid * :attr:`USER_ALREADY_LOGGED` if the user is already logged and do no request reauthentication. * :attr:`USER_LOGIN_FAILURE` if the user is not logged or request for reauthentication and his credentials are not valid * :attr:`USER_LOGIN_OK` if the user is not logged or request for reauthentication and his credentials are valid :rtype: int """ if not self.check_lt(): self.init_form(self.request.POST) logger.warning("Received an invalid login ticket") return self.INVALID_LOGIN_TICKET elif not self.request.session.get("authenticated") or self.renew: # authentication request receive, initialize the form to use self.init_form(self.request.POST) if self.form.is_valid(): self.request.session.set_expiry(0) self.request.session["username"] = self.form.cleaned_data['username'] self.request.session["warn"] = True if self.form.cleaned_data.get("warn") else False self.request.session["authenticated"] = True self.renewed = True self.warned = True logger.info("User %s successfully authenticated" % self.request.session["username"]) return self.USER_LOGIN_OK else: logger.warning("A login attempt failed") return self.USER_LOGIN_FAILURE else: logger.warning("Received a login attempt for an already-active user") return self.USER_ALREADY_LOGGED def init_get(self, request): """ Initialize GET received parameters :param django.http.HttpRequest request: The current request object """ self.request = request self.service = request.GET.get('service') self.renew = bool(request.GET.get('renew') and request.GET['renew'] != "False") self.gateway = request.GET.get('gateway') self.method = request.GET.get('method') self.ajax = settings.CAS_ENABLE_AJAX_AUTH and 'HTTP_X_AJAX' in request.META self.warn = request.GET.get('warn') if settings.CAS_FEDERATE: # here username and ticket are fetch from the session after a redirection from # FederateAuth.get self.username = request.session.get("federate_username") self.ticket = request.session.get("federate_ticket") if self.username: del request.session["federate_username"] if self.ticket: del request.session["federate_ticket"] def get(self, request, *args, **kwargs): """ method called on GET request on this view :param django.http.HttpRequest request: The current request object """ # initialize class parameters self.init_get(request) # process the GET request self.process_get() # call the GET/POST common part return self.common() def process_get(self): """ Analyse the GET request :return: * :attr:`USER_NOT_AUTHENTICATED` if the user is not authenticated or is requesting for authentication renewal * :attr:`USER_AUTHENTICATED` if the user is authenticated and is not requesting for authentication renewal :rtype: int """ # generate a new LT self.gen_lt() if not self.request.session.get("authenticated") or self.renew: # authentication will be needed, initialize the form to use self.init_form() return self.USER_NOT_AUTHENTICATED return self.USER_AUTHENTICATED def init_form(self, values=None): """ Initialization of the good form depending of POST and GET parameters :param django.http.QueryDict values: A POST or GET QueryDict """ if values: values = values.copy() values['lt'] = self.request.session['lt'][-1] form_initial = { 'service': self.service, 'method': self.method, 'warn': ( self.warn or self.request.session.get("warn") or self.request.COOKIES.get('warn') ), 'lt': self.request.session['lt'][-1], 'renew': self.renew } if settings.CAS_FEDERATE: if self.username and self.ticket: form_initial['username'] = self.username form_initial['password'] = self.ticket form_initial['ticket'] = self.ticket self.form = forms.FederateUserCredential( values, initial=form_initial ) else: self.form = forms.FederateSelect(values, initial=form_initial) else: self.form = forms.UserCredential( values, initial=form_initial ) def service_login(self): """ Perform login against a service :return: * The rendering of the ``settings.CAS_WARN_TEMPLATE`` if the user asked to be warned before ticket emission and has not yep been warned. * The redirection to the service URL with a ticket GET parameter * The redirection to the service URL without a ticket if ticket generation failed and the :attr:`gateway` attribute is set * The rendering of the ``settings.CAS_LOGGED_TEMPLATE`` template with some error messages if the ticket generation failed (e.g: user not allowed). :rtype: django.http.HttpResponse """ try: # is the service allowed service_pattern = ServicePattern.validate(self.service) # is the current user allowed on this service service_pattern.check_user(self.user) # if the user has asked to be warned before any login to a service if self.request.session.get("warn", True) and not self.warned: messages.add_message( self.request, messages.WARNING, _(u"Authentication has been required by service %(name)s (%(url)s)") % {'name': service_pattern.name, 'url': self.service} ) if self.ajax: data = {"status": "error", "detail": "confirmation needed"} return json_response(self.request, data) else: warn_form = forms.WarnForm(initial={ 'service': self.service, 'renew': self.renew, 'gateway': self.gateway, 'method': self.method, 'warned': True, 'lt': self.request.session['lt'][-1] }) return render( self.request, settings.CAS_WARN_TEMPLATE, utils.context({'form': warn_form}) ) else: # redirect, using method ? list(messages.get_messages(self.request)) # clean messages before leaving django redirect_url = self.user.get_service_url( self.service, service_pattern, renew=self.renewed ) if not self.ajax: return HttpResponseRedirect(redirect_url) else: data = {"status": "success", "detail": "auth", "url": redirect_url} return json_response(self.request, data) except ServicePattern.DoesNotExist: error = 1 messages.add_message( self.request, messages.ERROR, _(u'Service %(url)s not allowed.') % {'url': self.service} ) except models.BadUsername: error = 2 messages.add_message( self.request, messages.ERROR, _(u"Username not allowed") ) except models.BadFilter: error = 3 messages.add_message( self.request, messages.ERROR, _(u"User characteristics not allowed") ) except models.UserFieldNotDefined: error = 4 messages.add_message( self.request, messages.ERROR, _(u"The attribute %(field)s is needed to use" u" that service") % {'field': service_pattern.user_field} ) # if gateway is set and auth failed redirect to the service without authentication if self.gateway and not self.ajax: list(messages.get_messages(self.request)) # clean messages before leaving django return HttpResponseRedirect(self.service) if not self.ajax: return render( self.request, settings.CAS_LOGGED_TEMPLATE, utils.context({'session': self.request.session}) ) else: data = {"status": "error", "detail": "auth", "code": error} return json_response(self.request, data) def authenticated(self): """ Processing authenticated users :return: * The returned value of :meth:`service_login` if :attr:`service` is defined * The rendering of ``settings.CAS_LOGGED_TEMPLATE`` otherwise :rtype: django.http.HttpResponse """ # Try to get the current :class:`models.User` object for the current # session try: self.user = models.User.objects.get( username=self.request.session.get("username"), session_key=self.request.session.session_key ) # if not found, flush the session and redirect to the login page except models.User.DoesNotExist: logger.warning( "User %s seems authenticated but is not found in the database." % ( self.request.session.get("username"), ) ) self.logout() if self.ajax: data = { "status": "error", "detail": "login required", "url": utils.reverse_params("cas_server:login", params=self.request.GET) } return json_response(self.request, data) else: return utils.redirect_params("cas_server:login", params=self.request.GET) # if login against a service if self.service: return self.service_login() # else display the logged template else: if self.ajax: data = {"status": "success", "detail": "logged"} return json_response(self.request, data) else: return render( self.request, settings.CAS_LOGGED_TEMPLATE, utils.context({'session': self.request.session}) ) def not_authenticated(self): """ Processing non authenticated users :return: * The rendering of ``settings.CAS_LOGIN_TEMPLATE`` with various messages depending of GET/POST parameters * The redirection to :class:`FederateAuth` if ``settings.CAS_FEDERATE`` is ``True`` and the "remember my identity provider" cookie is found :rtype: django.http.HttpResponse """ if self.service: try: service_pattern = ServicePattern.validate(self.service) if self.gateway and not self.ajax: # clean messages before leaving django list(messages.get_messages(self.request)) return HttpResponseRedirect(self.service) if settings.CAS_SHOW_SERVICE_MESSAGES: if self.request.session.get("authenticated") and self.renew: messages.add_message( self.request, messages.WARNING, _(u"Authentication renewal required by service %(name)s (%(url)s).") % {'name': service_pattern.name, 'url': self.service} ) else: messages.add_message( self.request, messages.WARNING, _(u"Authentication required by service %(name)s (%(url)s).") % {'name': service_pattern.name, 'url': self.service} ) except ServicePattern.DoesNotExist: if settings.CAS_SHOW_SERVICE_MESSAGES: messages.add_message( self.request, messages.ERROR, _(u'Service %s not allowed') % self.service ) if self.ajax: data = { "status": "error", "detail": "login required", "url": utils.reverse_params("cas_server:login", params=self.request.GET) } return json_response(self.request, data) else: if settings.CAS_FEDERATE: if self.username and self.ticket: return render( self.request, settings.CAS_LOGIN_TEMPLATE, utils.context({ 'form': self.form, 'auto_submit': True, 'post_url': reverse("cas_server:login") }) ) else: if ( self.request.COOKIES.get('remember_provider') and FederatedIendityProvider.objects.filter( suffix=self.request.COOKIES['remember_provider'] ) ): params = utils.copy_params(self.request.GET) url = utils.reverse_params( "cas_server:federateAuth", params=params, kwargs=dict(provider=self.request.COOKIES['remember_provider']) ) return HttpResponseRedirect(url) else: # if user is authenticated and auth renewal is requested, redirect directly # to the user identity provider if self.renew and self.request.session.get("authenticated"): try: user = FederatedUser.get_from_federated_username( self.request.session.get("username") ) params = utils.copy_params(self.request.GET) url = utils.reverse_params( "cas_server:federateAuth", params=params, kwargs=dict(provider=user.provider.suffix) ) return HttpResponseRedirect(url) # Should normally not happen: if the user is logged, it exists in the # database. except FederatedUser.DoesNotExist: # pragma: no cover pass return render( self.request, settings.CAS_LOGIN_TEMPLATE, utils.context({ 'form': self.form, 'post_url': reverse("cas_server:federateAuth") }) ) else: return render( self.request, settings.CAS_LOGIN_TEMPLATE, utils.context({'form': self.form}) ) def common(self): """ Common part execute uppon GET and POST request :return: * The returned value of :meth:`authenticated` if the user is authenticated and not requesting for authentication or if the authentication has just been renewed * The returned value of :meth:`not_authenticated` otherwise :rtype: django.http.HttpResponse """ # if authenticated and successfully renewed authentication if needed if self.request.session.get("authenticated") and (not self.renew or self.renewed): return self.authenticated() else: return self.not_authenticated() class Auth(CsrfExemptView): """ A simple view to validate username/password/service tuple csrf is disable as it is intended to be used by programs. Security is assured by a shared secret between the programs dans django-cas-server. """ @staticmethod def post(request): """ method called on POST request on this view :param django.http.HttpRequest request: The current request object :return: ``HttpResponse(u"yes\\n")`` if the POSTed tuple (username, password, service) if valid (i.e. (username, password) is valid dans username is allowed on service). ``HttpResponse(u"no\\n…")`` otherwise, with possibly an error message on the second line. :rtype: django.http.HttpResponse """ username = request.POST.get('username') password = request.POST.get('password') service = request.POST.get('service') secret = request.POST.get('secret') if not settings.CAS_AUTH_SHARED_SECRET: return HttpResponse( "no\nplease set CAS_AUTH_SHARED_SECRET", content_type="text/plain; charset=utf-8" ) if secret != settings.CAS_AUTH_SHARED_SECRET: return HttpResponse(u"no\n", content_type="text/plain; charset=utf-8") if not username or not password or not service: return HttpResponse(u"no\n", content_type="text/plain; charset=utf-8") form = forms.UserCredential( request.POST, initial={ 'service': service, 'method': 'POST', 'warn': False } ) if form.is_valid(): try: user = models.User.objects.get_or_create( username=form.cleaned_data['username'], session_key=request.session.session_key )[0] user.save() # is the service allowed service_pattern = ServicePattern.validate(service) # is the current user allowed on this service service_pattern.check_user(user) if not request.session.get("authenticated"): user.delete() return HttpResponse(u"yes\n", content_type="text/plain; charset=utf-8") except (ServicePattern.DoesNotExist, models.ServicePatternException): return HttpResponse(u"no\n", content_type="text/plain; charset=utf-8") else: return HttpResponse(u"no\n", content_type="text/plain; charset=utf-8") class Validate(View): """service ticket validation""" @staticmethod def get(request): """ method called on GET request on this view :param django.http.HttpRequest request: The current request object :return: * ``HttpResponse("yes\\nusername")`` if submited (service, ticket) is valid * else ``HttpResponse("no\\n")`` :rtype: django.http.HttpResponse """ # store wanted GET parameters service = request.GET.get('service') ticket = request.GET.get('ticket') renew = True if request.GET.get('renew') else False # service and ticket parameters are mandatory if service and ticket: try: # search for the ticket, associated at service that is not yet validated but is # still valid ticket = ServiceTicket.get(ticket, renew, service) logger.info( "Validate: Service ticket %s validated, user %s authenticated on service %s" % ( ticket.value, ticket.user.username, ticket.service ) ) return HttpResponse( u"yes\n%s\n" % ticket.username(), content_type="text/plain; charset=utf-8" ) except ServiceTicket.DoesNotExist: logger.warning( ( "Validate: Service ticket %s not found or " "already validated, auth to %s failed" ) % ( ticket, service ) ) return HttpResponse(u"no\n", content_type="text/plain; charset=utf-8") else: logger.warning("Validate: service or ticket missing") return HttpResponse(u"no\n", content_type="text/plain; charset=utf-8") @python_2_unicode_compatible class ValidationBaseError(Exception): """Base class for both saml and cas validation error""" #: The error code code = None #: The error message msg = None def __init__(self, code, msg=""): self.code = code self.msg = msg super(ValidationBaseError, self).__init__(code) def __str__(self): return u"%s" % self.msg def render(self, request): """ render the error template for the exception :param django.http.HttpRequest request: The current request object: :return: the rendered ``cas_server/serviceValidateError.xml`` template :rtype: django.http.HttpResponse """ return render( request, self.template, self.context(), content_type="text/xml; charset=utf-8" ) class ValidateError(ValidationBaseError): """handle service validation error""" #: template to be render for the error template = "cas_server/serviceValidateError.xml" def context(self): """ content to use to render :attr:`template` :return: A dictionary to contextualize :attr:`template` :rtype: dict """ return {'code': self.code, 'msg': self.msg} class ValidateService(View): """service ticket validation [CAS 2.0] and [CAS 3.0]""" #: Current :class:`django.http.HttpRequest` object request = None #: The service GET parameter service = None #: the ticket GET parameter ticket = None #: the pgtUrl GET parameter pgt_url = None #: the renew GET parameter renew = None #: specify if ProxyTicket are allowed by the view. Hence we user the same view for #: ``/serviceValidate`` and ``/proxyValidate`` juste changing the parameter. allow_proxy_ticket = False def get(self, request): """ method called on GET request on this view :param django.http.HttpRequest request: The current request object: :return: The rendering of ``cas_server/serviceValidate.xml`` if no errors is raised, the rendering or ``cas_server/serviceValidateError.xml`` otherwise. :rtype: django.http.HttpResponse """ # define the class parameters self.request = request self.service = request.GET.get('service') self.ticket = request.GET.get('ticket') self.pgt_url = request.GET.get('pgtUrl') self.renew = True if request.GET.get('renew') else False # service and ticket parameter are mandatory if not self.service or not self.ticket: logger.warning("ValidateService: missing ticket or service") return ValidateError( u'INVALID_REQUEST', u"you must specify a service and a ticket" ).render(request) else: try: # search the ticket in the database self.ticket, proxies = self.process_ticket() # prepare template rendering context params = { 'username': self.ticket.username(), 'attributes': self.ticket.attributs_flat(), 'proxies': proxies, 'auth_date': self.ticket.user.last_login.replace(microsecond=0).isoformat(), 'is_new_login': 'true' if self.ticket.renew else 'false' } # if pgtUrl is set, require https or localhost if self.pgt_url and ( self.pgt_url.startswith("https://") or re.match(r"^http://(127\.0\.0\.1|localhost)(:[0-9]+)?(/.*)?$", self.pgt_url) ): return self.process_pgturl(params) else: logger.info( "ValidateService: ticket %s validated for user %s on service %s." % ( self.ticket.value, self.ticket.user.username, self.ticket.service ) ) logger.debug( "ValidateService: User attributs are:\n%s" % ( pprint.pformat(self.ticket.attributs), ) ) return render( request, "cas_server/serviceValidate.xml", params, content_type="text/xml; charset=utf-8" ) except ValidateError as error: logger.warning( "ValidateService: validation error: %s %s" % (error.code, error.msg) ) return error.render(request) def process_ticket(self): """ fetch the ticket against the database and check its validity :raises ValidateError: if the ticket is not found or not valid, potentially for that service :returns: A couple (ticket, proxies list) :rtype: :obj:`tuple` """ try: proxies = [] if self.allow_proxy_ticket: ticket = models.Ticket.get(self.ticket, self.renew) else: ticket = models.ServiceTicket.get(self.ticket, self.renew) try: for prox in ticket.proxies.all(): proxies.append(prox.url) except AttributeError: pass if ticket.service != self.service: raise ValidateError(u'INVALID_SERVICE', self.service) return ticket, proxies except Ticket.DoesNotExist: raise ValidateError(u'INVALID_TICKET', self.ticket) except (ServiceTicket.DoesNotExist, ProxyTicket.DoesNotExist): raise ValidateError(u'INVALID_TICKET', 'ticket not found') def process_pgturl(self, params): """ Handle PGT request :param dict params: A template context dict :raises ValidateError: if pgtUrl is invalid or if TLS validation of the pgtUrl fails :return: The rendering of ``cas_server/serviceValidate.xml``, using ``params`` :rtype: django.http.HttpResponse """ try: pattern = ServicePattern.validate(self.pgt_url) if pattern.proxy_callback: proxyid = utils.gen_pgtiou() pticket = ProxyGrantingTicket.objects.create( user=self.ticket.user, service=self.pgt_url, service_pattern=pattern, single_log_out=pattern.single_log_out ) url = utils.update_url(self.pgt_url, {'pgtIou': proxyid, 'pgtId': pticket.value}) try: ret = requests.get(url, verify=settings.CAS_PROXY_CA_CERTIFICATE_PATH) if ret.status_code == 200: params['proxyGrantingTicket'] = proxyid else: pticket.delete() logger.info( ( "ValidateService: ticket %s validated for user %s on service %s. " "Proxy Granting Ticket transmited to %s." ) % ( self.ticket.value, self.ticket.user.username, self.ticket.service, self.pgt_url ) ) logger.debug( "ValidateService: User attributs are:\n%s" % ( pprint.pformat(self.ticket.attributs), ) ) return render( self.request, "cas_server/serviceValidate.xml", params, content_type="text/xml; charset=utf-8" ) except requests.exceptions.RequestException as error: error = utils.unpack_nested_exception(error) raise ValidateError( u'INVALID_PROXY_CALLBACK', u"%s: %s" % (type(error), str(error)) ) else: raise ValidateError( u'INVALID_PROXY_CALLBACK', u"callback url not allowed by configuration" ) except ServicePattern.DoesNotExist: raise ValidateError( u'INVALID_PROXY_CALLBACK', u'callback url not allowed by configuration' ) class Proxy(View): """proxy ticket service""" #: Current :class:`django.http.HttpRequest` object request = None #: A ProxyGrantingTicket from the pgt GET parameter pgt = None #: the targetService GET parameter target_service = None def get(self, request): """ method called on GET request on this view :param django.http.HttpRequest request: The current request object: :return: The returned value of :meth:`process_proxy` if no error is raised, else the rendering of ``cas_server/serviceValidateError.xml``. :rtype: django.http.HttpResponse """ self.request = request self.pgt = request.GET.get('pgt') self.target_service = request.GET.get('targetService') try: # pgt and targetService parameters are mandatory if self.pgt and self.target_service: return self.process_proxy() else: raise ValidateError( u'INVALID_REQUEST', u"you must specify and pgt and targetService" ) except ValidateError as error: logger.warning("Proxy: validation error: %s %s" % (error.code, error.msg)) return error.render(request) def process_proxy(self): """ handle PT request :raises ValidateError: if the PGT is not found, or the target service not allowed or the user not allowed on the tardet service. :return: The rendering of ``cas_server/proxy.xml`` :rtype: django.http.HttpResponse """ try: # is the target service allowed pattern = ServicePattern.validate(self.target_service) # to get a proxy ticket require that the service allow it if not pattern.proxy: raise ValidateError( u'UNAUTHORIZED_SERVICE', u'the service %s does not allow proxy tickets' % self.target_service ) # is the proxy granting ticket valid ticket = ProxyGrantingTicket.get(self.pgt) # is the pgt user allowed on the target service pattern.check_user(ticket.user) pticket = ticket.user.get_ticket( ProxyTicket, self.target_service, pattern, renew=False ) models.Proxy.objects.create(proxy_ticket=pticket, url=ticket.service) logger.info( "Proxy ticket created for user %s on service %s." % ( ticket.user.username, self.target_service ) ) return render( self.request, "cas_server/proxy.xml", {'ticket': pticket.value}, content_type="text/xml; charset=utf-8" ) except (Ticket.DoesNotExist, ProxyGrantingTicket.DoesNotExist): raise ValidateError(u'INVALID_TICKET', u'PGT %s not found' % self.pgt) except ServicePattern.DoesNotExist: raise ValidateError(u'UNAUTHORIZED_SERVICE', self.target_service) except (models.BadUsername, models.BadFilter, models.UserFieldNotDefined): raise ValidateError( u'UNAUTHORIZED_USER', u'User %s not allowed on %s' % (ticket.user.username, self.target_service) ) class SamlValidateError(ValidationBaseError): """handle saml validation error""" #: template to be render for the error template = "cas_server/samlValidateError.xml" def context(self): """ :return: A dictionary to contextualize :attr:`template` :rtype: dict """ return { 'code': self.code, 'msg': self.msg, 'IssueInstant': timezone.now().isoformat(), 'ResponseID': utils.gen_saml_id() } class SamlValidate(CsrfExemptView): """SAML ticket validation""" request = None target = None ticket = None root = None def post(self, request): """ method called on POST request on this view :param django.http.HttpRequest request: The current request object :return: the rendering of ``cas_server/samlValidate.xml`` if no error is raised, else the rendering of ``cas_server/samlValidateError.xml``. :rtype: django.http.HttpResponse """ self.request = request self.target = request.GET.get('TARGET') self.root = etree.fromstring(request.body) try: self.ticket = self.process_ticket() expire_instant = (self.ticket.creation + timedelta(seconds=self.ticket.VALIDITY)).isoformat() params = { 'IssueInstant': timezone.now().isoformat(), 'expireInstant': expire_instant, 'Recipient': self.target, 'ResponseID': utils.gen_saml_id(), 'username': self.ticket.username(), 'attributes': self.ticket.attributs_flat(), 'auth_date': self.ticket.user.last_login.replace(microsecond=0).isoformat(), 'is_new_login': 'true' if self.ticket.renew else 'false' } logger.info( "SamlValidate: ticket %s validated for user %s on service %s." % ( self.ticket.value, self.ticket.user.username, self.ticket.service ) ) logger.debug( "SamlValidate: User attributes are:\n%s" % pprint.pformat(self.ticket.attributs) ) return render( request, "cas_server/samlValidate.xml", params, content_type="text/xml; charset=utf-8" ) except SamlValidateError as error: logger.warning("SamlValidate: validation error: %s %s" % (error.code, error.msg)) return error.render(request) def process_ticket(self): """ validate ticket from SAML XML body :raises: SamlValidateError: if the ticket is not found or not valid, or if we fail to parse the posted XML. :return: a ticket object :rtype: :class:`models.Ticket` """ try: auth_req = self.root.getchildren()[1].getchildren()[0] ticket = auth_req.getchildren()[0].text ticket = models.Ticket.get(ticket) if ticket.service != self.target: raise SamlValidateError( u'AuthnFailed', u'TARGET %s does not match ticket service' % self.target ) return ticket except (IndexError, KeyError): raise SamlValidateError(u'VersionMismatch') except Ticket.DoesNotExist: raise SamlValidateError( u'AuthnFailed', u'ticket %s should begin with PT- or ST-' % ticket ) except (ServiceTicket.DoesNotExist, ProxyTicket.DoesNotExist): raise SamlValidateError(u'AuthnFailed', u'ticket %s not found' % ticket) django-cas-server-1.1.0/cas_server/templates/0000755000175000017500000000000013436457571022656 5ustar valentinvalentin00000000000000django-cas-server-1.1.0/cas_server/templates/cas_server/0000755000175000017500000000000013436457571025012 5ustar valentinvalentin00000000000000django-cas-server-1.1.0/cas_server/templates/cas_server/serviceValidate.xml0000644000175000017500000000223313416345433030635 0ustar valentinvalentin00000000000000 {{username}} {{auth_date}} false{# we do not support long-term (Remember-Me) auth #} {{is_new_login}} {% for key, value in attributes %} {{value}} {% endfor %} {% for key, value in attributes %} {% endfor %}{% if proxyGrantingTicket %} {{proxyGrantingTicket}} {% endif %}{% if proxies %} {% for proxy in proxies %} {{proxy}} {% endfor %} {% endif %} django-cas-server-1.1.0/cas_server/templates/cas_server/logout.html0000644000175000017500000000025613436457341027207 0ustar valentinvalentin00000000000000{% extends "cas_server/base.html" %} {% load static %} {% load i18n %} {% block content %} {% endblock %} django-cas-server-1.1.0/cas_server/templates/cas_server/serviceValidateError.xml0000644000175000017500000000024412632536713031651 0ustar valentinvalentin00000000000000 {{msg}} django-cas-server-1.1.0/cas_server/templates/cas_server/warn.html0000644000175000017500000000051313436457341026641 0ustar valentinvalentin00000000000000{% extends "cas_server/base.html" %} {% load static %} {% load i18n %} {% block content %} {% endblock %} django-cas-server-1.1.0/cas_server/templates/cas_server/base.html0000644000175000017500000001162513436457341026612 0ustar valentinvalentin00000000000000{% load i18n %}{% load static %}{% get_current_language as LANGUAGE_CODE %} {% block title %}{% trans "Central Authentication Service" %}{% endblock %} {% if settings.CAS_FAVICON_URL %}{% endif %}
{% if auto_submit %}{% endif %}
{% if auto_submit %}{% endif %} {% block content %}{% endblock %}
{% if settings.CAS_SHOW_POWERED %} {% endif %} {% block javascript %}{% endblock %} django-cas-server-1.1.0/cas_server/templates/cas_server/login.html0000644000175000017500000000174712757406463027020 0ustar valentinvalentin00000000000000{% extends "cas_server/base.html" %} {% load i18n %} {% block ante_messages %} {% if auto_submit %}{% endif %} {% endblock %} {% block content %} {% endblock %} {% block javascript_inline %} jQuery(function( $ ){ $("#id_warn").click(function(e){ if($("#id_warn").is(':checked')){ createCookie("warn", "on", 10 * 365); } else { eraseCookie("warn"); } }); });{% if auto_submit %} document.getElementById('login_form').submit(); // SUBMIT FORM{% endif %} {% endblock %} django-cas-server-1.1.0/cas_server/templates/cas_server/logged.html0000644000175000017500000000170712757022243027133 0ustar valentinvalentin00000000000000{% extends "cas_server/base.html" %} {% load i18n %} {% block content %} {% endblock %} django-cas-server-1.1.0/cas_server/templates/cas_server/form.html0000644000175000017500000000157313060003340026617 0ustar valentinvalentin00000000000000{% load cas_server %} {% for error in form.non_field_errors %}
{{error}}
{% endfor %} {% for field in form %}{% if not field|is_hidden %}
{% spaceless %} {% if field|is_checkbox %}
{% else %} {{field}} {% endif %} {% for error in field.errors %} {{error}} {% endfor %} {% endspaceless %}
{% else %}{{field}}{% endif %}{% endfor %} django-cas-server-1.1.0/cas_server/templates/cas_server/samlValidate.xml0000644000175000017500000000544213416345433030136 0ustar valentinvalentin00000000000000 {{Recipient}} {{username}} urn:oasis:names:tc:SAML:1.0:cm:artifact {{auth_date}} false{# we do not support long-term (Remember-Me) auth #} {{is_new_login}} {% for name, value in attributes %} {{value}} {% endfor %} {{username}} urn:oasis:names:tc:SAML:1.0:cm:artifact django-cas-server-1.1.0/cas_server/templates/cas_server/samlValidateError.xml0000644000175000017500000000123312632536713031144 0ustar valentinvalentin00000000000000 {{msg}} django-cas-server-1.1.0/cas_server/templates/cas_server/proxy.xml0000644000175000017500000000030112632536713026700 0ustar valentinvalentin00000000000000 {{ticket}} django-cas-server-1.1.0/cas_server/default_settings.py0000644000175000017500000002533413436457341024600 0ustar valentinvalentin00000000000000# -*- coding: utf-8 -*- # 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 General Public License version 3 for # more details. # # You should have received a copy of the GNU General Public License version 3 # along with this program; if not, write to the Free Software Foundation, Inc., 51 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # (c) 2015-2016 Valentin Samir """Default values for the app's settings""" from django.conf import settings from django.templatetags.static import static from django.utils.translation import ugettext_lazy as _ from importlib import import_module try: #: URL to the logo showed in the up left corner on the default templates. CAS_LOGO_URL = static("cas_server/logo.png") #: URL to the favicon (shortcut icon) used by the default templates. Default is a key icon. CAS_FAVICON_URL = static("cas_server/favicon.ico") # is settings.DEBUG is False and collectstatics has not been run yet, the static function will # raise a ValueError because the file is not found. except ValueError: #: URL to the logo showed in the up left corner on the default templates. CAS_LOGO_URL = None #: URL to the favicon (shortcut icon) used by the default templates. Default is a key icon. CAS_FAVICON_URL = None #: Show the powered by footer if set to ``True`` CAS_SHOW_POWERED = True #: URLs to css and javascript external components. CAS_COMPONENT_URLS = { "bootstrap3_css": "//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css", "bootstrap3_js": "//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js", "html5shiv": "//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js", "respond": "//oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js", "jquery": "//code.jquery.com/jquery.min.js", } #: Path to the template showed on /login then the user is not autenticated. CAS_LOGIN_TEMPLATE = 'cas_server/login.html' #: Path to the template showed on /login?service=... then the user is authenticated and has asked #: to be warned before being connected to a service. CAS_WARN_TEMPLATE = 'cas_server/warn.html' #: Path to the template showed on /login then to user is authenticated. CAS_LOGGED_TEMPLATE = 'cas_server/logged.html' #: Path to the template showed on /logout then to user is being disconnected. CAS_LOGOUT_TEMPLATE = 'cas_server/logout.html' #: Should we redirect users to /login after they logged out instead of displaying #: :obj:`CAS_LOGOUT_TEMPLATE`. CAS_REDIRECT_TO_LOGIN_AFTER_LOGOUT = False #: A dotted path to a class or a class implementing cas_server.auth.AuthUser. CAS_AUTH_CLASS = 'cas_server.auth.DjangoAuthUser' #: Path to certificate authorities file. Usually on linux the local CAs are in #: /etc/ssl/certs/ca-certificates.crt. ``True`` tell requests to use its internal certificat #: authorities. CAS_PROXY_CA_CERTIFICATE_PATH = True #: Maximum number of parallel single log out requests send #: if more requests need to be send, there are queued CAS_SLO_MAX_PARALLEL_REQUESTS = 10 #: Timeout for a single SLO request in seconds. CAS_SLO_TIMEOUT = 5 #: Shared to transmit then using the view :class:`cas_server.views.Auth` CAS_AUTH_SHARED_SECRET = '' #: Max time after with the user MUST reauthenticate. Let it to `None` for no max time. #: This can be used to force refreshing cached informations only available upon user authentication #: like the user attributes in federation mode or with the ldap auth in bind mode. CAS_TGT_VALIDITY = None #: Number of seconds the service tickets and proxy tickets are valid. This is the maximal time #: between ticket issuance by the CAS and ticket validation by an application. CAS_TICKET_VALIDITY = 60 #: Number of seconds the proxy granting tickets are valid. CAS_PGT_VALIDITY = 3600 #: Number of seconds a ticket is kept in the database before sending Single Log Out request and #: being cleared. CAS_TICKET_TIMEOUT = 24*3600 #: All CAS implementation MUST support ST and PT up to 32 chars, #: PGT and PGTIOU up to 64 chars and it is RECOMMENDED that all #: tickets up to 256 chars are supports so we use 64 for the default #: len. CAS_TICKET_LEN = 64 #: alias of :obj:`settings.CAS_TICKET_LEN` CAS_LT_LEN = getattr(settings, 'CAS_TICKET_LEN', CAS_TICKET_LEN) #: alias of :obj:`settings.CAS_TICKET_LEN` #: Services MUST be able to accept service tickets of up to 32 characters in length. CAS_ST_LEN = getattr(settings, 'CAS_TICKET_LEN', CAS_TICKET_LEN) #: alias of :obj:`settings.CAS_TICKET_LEN` #: Back-end services MUST be able to accept proxy tickets of up to 32 characters. CAS_PT_LEN = getattr(settings, 'CAS_TICKET_LEN', CAS_TICKET_LEN) #: alias of :obj:`settings.CAS_TICKET_LEN` #: Services MUST be able to handle proxy-granting tickets of up to 64 CAS_PGT_LEN = getattr(settings, 'CAS_TICKET_LEN', CAS_TICKET_LEN) #: alias of :obj:`settings.CAS_TICKET_LEN` #: Services MUST be able to handle PGTIOUs of up to 64 characters in length. CAS_PGTIOU_LEN = getattr(settings, 'CAS_TICKET_LEN', CAS_TICKET_LEN) #: Prefix of login tickets. CAS_LOGIN_TICKET_PREFIX = u'LT' #: Prefix of service tickets. Service tickets MUST begin with the characters ST so you should not #: change this. CAS_SERVICE_TICKET_PREFIX = u'ST' #: Prefix of proxy ticket. Proxy tickets SHOULD begin with the characters, PT. CAS_PROXY_TICKET_PREFIX = u'PT' #: Prefix of proxy granting ticket. Proxy-granting tickets SHOULD begin with the characters PGT. CAS_PROXY_GRANTING_TICKET_PREFIX = u'PGT' #: Prefix of proxy granting ticket IOU. Proxy-granting ticket IOUs SHOULD begin with the characters #: PGTIOU. CAS_PROXY_GRANTING_TICKET_IOU_PREFIX = u'PGTIOU' #: Host for the SQL server. CAS_SQL_HOST = 'localhost' #: Username for connecting to the SQL server. CAS_SQL_USERNAME = '' #: Password for connecting to the SQL server. CAS_SQL_PASSWORD = '' #: Database name. CAS_SQL_DBNAME = '' #: Database charset. CAS_SQL_DBCHARSET = 'utf8' #: The query performed upon user authentication. CAS_SQL_USER_QUERY = 'SELECT user AS username, pass AS password, users.* FROM users WHERE user = %s' #: The method used to check the user password. Must be one of ``"crypt"``, ``"ldap"``, #: ``"hex_md5"``, ``"hex_sha1"``, ``"hex_sha224"``, ``"hex_sha256"``, ``"hex_sha384"``, #: ``"hex_sha512"``, ``"plain"``. CAS_SQL_PASSWORD_CHECK = 'crypt' #: charset the SQL users passwords was hash with CAS_SQL_PASSWORD_CHARSET = "utf-8" #: Address of the LDAP server CAS_LDAP_SERVER = 'localhost' #: LDAP user bind address, for example ``"cn=admin,dc=crans,dc=org"`` for connecting to the LDAP #: server. CAS_LDAP_USER = None #: LDAP connection password CAS_LDAP_PASSWORD = None #: LDAP seach base DN, for example ``"ou=data,dc=crans,dc=org"``. CAS_LDAP_BASE_DN = None #: LDAP search filter for searching user by username. User inputed usernames are escaped using #: :func:`ldap3.utils.conv.escape_bytes`. CAS_LDAP_USER_QUERY = "(uid=%s)" #: LDAP attribute used for users usernames CAS_LDAP_USERNAME_ATTR = "uid" #: LDAP attribute used for users passwords CAS_LDAP_PASSWORD_ATTR = "userPassword" #: The method used to check the user password. Must be one of ``"crypt"``, ``"ldap"``, #: ``"hex_md5"``, ``"hex_sha1"``, ``"hex_sha224"``, ``"hex_sha256"``, ``"hex_sha384"``, #: ``"hex_sha512"``, ``"plain"``. CAS_LDAP_PASSWORD_CHECK = "ldap" #: charset the LDAP users passwords was hash with CAS_LDAP_PASSWORD_CHARSET = "utf-8" #: Username of the test user. CAS_TEST_USER = 'test' #: Password of the test user. CAS_TEST_PASSWORD = 'test' #: Attributes of the test user. CAS_TEST_ATTRIBUTES = { 'nom': 'Nymous', 'prenom': 'Ano', 'email': 'anonymous@example.net', 'alias': ['demo1', 'demo2'] } #: A :class:`bool` for activatinc the hability to fetch tickets using javascript. CAS_ENABLE_AJAX_AUTH = False #: A :class:`bool` for activating the federated mode CAS_FEDERATE = False #: Time after witch the cookie use for “remember my identity provider” expire (one week). CAS_FEDERATE_REMEMBER_TIMEOUT = 604800 #: A :class:`bool` for diplaying a warning on html pages then a new version of the application #: is avaible. Once closed by a user, it is not displayed to this user until the next new version. CAS_NEW_VERSION_HTML_WARNING = True #: A :class:`bool` for sending emails to ``settings.ADMINS`` when a new version is available. CAS_NEW_VERSION_EMAIL_WARNING = True #: URL to the pypi json of the application. Used to retreive the version number of the last version. #: You should not change it. CAS_NEW_VERSION_JSON_URL = "https://pypi.org/pypi/django-cas-server/json" #: If the service message should be displayed on the login page CAS_SHOW_SERVICE_MESSAGES = True #: Messages displayed in a info-box on the html pages of the default templates. #: ``CAS_INFO_MESSAGES`` is a :class:`dict` mapping message name to a message :class:`dict`. #: A message :class:`dict` has 3 keys: #: * ``message``: A :class:`unicode`, the message to display, potentially wrapped around #: ugettex_lazy #: * ``discardable``: A :class:`bool`, specify if the users can close the message info-box #: * ``type``: One of info, success, info, warning, danger. The type of the info-box. #: ``CAS_INFO_MESSAGES`` contains by default one message, ``cas_explained``, which explain #: roughly the purpose of a CAS. CAS_INFO_MESSAGES = { "cas_explained": { "message": _( u"The Central Authentication Service grants you access to most of our websites by " u"authenticating only once, so you don't need to type your credentials again unless " u"your session expires or you logout." ), "discardable": True, "type": "info", # one of info, success, info, warning, danger }, } #: :class:`list` of message names. Order in which info-box messages are displayed. #: Let the list empty to disable messages display. CAS_INFO_MESSAGES_ORDER = [] GLOBALS = globals().copy() for name, default_value in GLOBALS.items(): # only care about parameter begining by CAS_ if name.startswith("CAS_"): # get the current setting value, falling back to default_value value = getattr(settings, name, default_value) # set the setting value to its value if defined, ellse to the default_value. setattr(settings, name, value) # Allow the user defined CAS_COMPONENT_URLS to omit not changed values MERGED_CAS_COMPONENT_URLS = CAS_COMPONENT_URLS.copy() MERGED_CAS_COMPONENT_URLS.update(settings.CAS_COMPONENT_URLS) settings.CAS_COMPONENT_URLS = MERGED_CAS_COMPONENT_URLS # if the federated mode is enabled, we must use the :class`cas_server.auth.CASFederateAuth` auth # backend. if settings.CAS_FEDERATE: settings.CAS_AUTH_CLASS = "cas_server.auth.CASFederateAuth" #: SessionStore class depending of :django:setting:`SESSION_ENGINE` SessionStore = import_module(settings.SESSION_ENGINE).SessionStore django-cas-server-1.1.0/cas_server/apps.py0000644000175000017500000000172112757022243022163 0ustar valentinvalentin00000000000000# 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 General Public License version 3 for # more details. # # You should have received a copy of the GNU General Public License version 3 # along with this program; if not, write to the Free Software Foundation, Inc., 51 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # (c) 2015-2016 Valentin Samir """django config module""" from django.utils.translation import ugettext_lazy as _ from django.apps import AppConfig class CasAppConfig(AppConfig): """ Bases: :class:`django.apps.AppConfig` django CAS application config class """ #: Full Python path to the application. It must be unique across a Django project. name = 'cas_server' #: Human-readable name for the application. verbose_name = _('Central Authentication Service') django-cas-server-1.1.0/cas_server/federate.py0000644000175000017500000001333513060003340022764 0ustar valentinvalentin00000000000000# -*- coding: utf-8 -*- # 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 General Public License version 3 for # more details. # # You should have received a copy of the GNU General Public License version 3 # along with this program; if not, write to the Free Software Foundation, Inc., 51 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # (c) 2016 Valentin Samir """federated mode helper classes""" from .default_settings import SessionStore from django.db import IntegrityError from .cas import CASClient from .models import FederatedUser, FederateSLO, User import logging from six.moves import urllib #: logger facility logger = logging.getLogger(__name__) class CASFederateValidateUser(object): """ Class CAS client used to authenticate the user again a CAS provider :param cas_server.models.FederatedIendityProvider provider: The provider to use for authenticate the user. :param unicode service_url: The service url to transmit to the ``provider``. """ #: the provider returned username username = None #: the provider returned attributes attributs = {} #: the CAS client instance client = None #: the provider returned username this the provider suffix appended federated_username = None #: the identity provider provider = None def __init__(self, provider, service_url, renew=False): self.provider = provider self.client = CASClient( service_url=service_url, version=provider.cas_protocol_version, server_url=provider.server_url, renew=renew, ) def get_login_url(self): """ :return: the CAS provider login url :rtype: unicode """ return self.client.get_login_url() def get_logout_url(self, redirect_url=None): """ :param redirect_url: The url to redirect to after logout from the provider, if provided. :type redirect_url: :obj:`unicode` or :obj:`NoneType` :return: the CAS provider logout url :rtype: unicode """ return self.client.get_logout_url(redirect_url) def verify_ticket(self, ticket): """ test ``ticket`` against the CAS provider, if valid, create a :class:`FederatedUser` matching provider returned username and attributes. :param unicode ticket: The ticket to validate against the provider CAS :return: ``True`` if the validation succeed, else ``False``. :rtype: bool """ try: username, attributs = self.client.verify_ticket(ticket)[:2] except urllib.error.URLError: return False if username is not None: if attributs is None: attributs = {} attributs["provider"] = self.provider.suffix self.username = username self.attributs = attributs user = FederatedUser.objects.update_or_create( username=username, provider=self.provider, defaults=dict(attributs=attributs, ticket=ticket) )[0] user.save() self.federated_username = user.federated_username return True else: return False @staticmethod def register_slo(username, session_key, ticket): """ association a ``ticket`` with a (``username``, ``session_key``) for processing later SLO request by creating a :class:`cas_server.models.FederateSLO` object. :param unicode username: A logged user username, with the ``@`` component. :param unicode session_key: A logged user session_key matching ``username``. :param unicode ticket: A ticket used to authentication ``username`` for the session ``session_key``. """ try: FederateSLO.objects.create( username=username, session_key=session_key, ticket=ticket ) except IntegrityError: # pragma: no cover (ignore if the FederateSLO already exists) pass def clean_sessions(self, logout_request): """ process a SLO request: Search for ticket values in ``logout_request``. For each ticket value matching a :class:`cas_server.models.FederateSLO`, disconnect the corresponding user. :param unicode logout_request: An XML document contening one or more Single Log Out requests. """ try: slos = self.client.get_saml_slos(logout_request) or [] except NameError: # pragma: no cover (should not happen) slos = [] for slo in slos: for federate_slo in FederateSLO.objects.filter(ticket=slo.text): logger.info( "Got an SLO requests for ticket %s, logging out user %s" % ( federate_slo.username, federate_slo.ticket ) ) session = SessionStore(session_key=federate_slo.session_key) session.flush() try: user = User.objects.get( username=federate_slo.username, session_key=federate_slo.session_key ) user.logout() user.delete() except User.DoesNotExist: # pragma: no cover (should not happen) pass federate_slo.delete() django-cas-server-1.1.0/cas_server/cas.py0000644000175000017500000003460213416345433021774 0ustar valentinvalentin00000000000000# Copyright (C) 2014, Ming Chen # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is furnished # to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in all # copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS # IN THE SOFTWARE. # This file is originated from https://github.com/python-cas/python-cas # at commit ec1f2d4779625229398547b9234d0e9e874a2c9a # some modifications have been made to be unicode coherent between python2 and python2 import six from six.moves.urllib import parse as urllib_parse from six.moves.urllib import request as urllib_request from six.moves.urllib.request import Request from uuid import uuid4 import datetime class CASError(ValueError): pass class ReturnUnicode(object): @staticmethod def u(string, charset): if not isinstance(string, six.text_type): return string.decode(charset) else: return string class SingleLogoutMixin(object): @classmethod def get_saml_slos(cls, logout_request): """returns saml logout ticket info""" from lxml import etree try: root = etree.fromstring(logout_request) return root.xpath( "//samlp:SessionIndex", namespaces={'samlp': "urn:oasis:names:tc:SAML:2.0:protocol"}) except etree.XMLSyntaxError: pass class CASClient(object): def __new__(self, *args, **kwargs): version = kwargs.pop('version') if version in (1, '1'): return CASClientV1(*args, **kwargs) elif version in (2, '2'): return CASClientV2(*args, **kwargs) elif version in (3, '3'): return CASClientV3(*args, **kwargs) elif version == 'CAS_2_SAML_1_0': return CASClientWithSAMLV1(*args, **kwargs) raise ValueError('Unsupported CAS_VERSION %r' % version) class CASClientBase(object): logout_redirect_param_name = 'service' def __init__(self, service_url=None, server_url=None, extra_login_params=None, renew=False, username_attribute=None): self.service_url = service_url self.server_url = server_url self.extra_login_params = extra_login_params or {} self.renew = renew self.username_attribute = username_attribute pass def verify_ticket(self, ticket): """must return a triple""" raise NotImplementedError() def get_login_url(self): """Generates CAS login URL""" params = {'service': self.service_url} if self.renew: params.update({'renew': 'true'}) params.update(self.extra_login_params) url = urllib_parse.urljoin(self.server_url, 'login') query = urllib_parse.urlencode(params) return url + '?' + query def get_logout_url(self, redirect_url=None): """Generates CAS logout URL""" url = urllib_parse.urljoin(self.server_url, 'logout') if redirect_url: params = {self.logout_redirect_param_name: redirect_url} url += '?' + urllib_parse.urlencode(params) return url def get_proxy_url(self, pgt): """Returns proxy url, given the proxy granting ticket""" params = urllib_parse.urlencode({'pgt': pgt, 'targetService': self.service_url}) return "%s/proxy?%s" % (self.server_url, params) def get_proxy_ticket(self, pgt): """Returns proxy ticket given the proxy granting ticket""" response = urllib_request.urlopen(self.get_proxy_url(pgt)) if response.code == 200: from lxml import etree root = etree.fromstring(response.read()) tickets = root.xpath( "//cas:proxyTicket", namespaces={"cas": "http://www.yale.edu/tp/cas"} ) if len(tickets) == 1: return tickets[0].text errors = root.xpath( "//cas:authenticationFailure", namespaces={"cas": "http://www.yale.edu/tp/cas"} ) if len(errors) == 1: raise CASError(errors[0].attrib['code'], errors[0].text) raise CASError("Bad http code %s" % response.code) @staticmethod def get_page_charset(page, default="utf-8"): content_type = page.info().get('Content-type') if content_type and "charset=" in content_type: return content_type.split("charset=")[-1] else: return default class CASClientV1(CASClientBase, ReturnUnicode): """CAS Client Version 1""" logout_redirect_param_name = 'url' def verify_ticket(self, ticket): """Verifies CAS 1.0 authentication ticket. Returns username on success and None on failure. """ params = [('ticket', ticket), ('service', self.service_url)] if self.renew: params.append(('renew', 'true')) url = (urllib_parse.urljoin(self.server_url, 'validate') + '?' + urllib_parse.urlencode(params)) page = urllib_request.urlopen(url) try: verified = page.readline().strip() if verified == b'yes': charset = self.get_page_charset(page, default="ascii") user = self.u(page.readline().strip(), charset) return user, None, None else: return None, None, None finally: page.close() class CASClientV2(CASClientBase, ReturnUnicode): """CAS Client Version 2""" url_suffix = 'serviceValidate' logout_redirect_param_name = 'url' def __init__(self, proxy_callback=None, *args, **kwargs): """proxy_callback is for V2 and V3 so V3 is subclass of V2""" self.proxy_callback = proxy_callback super(CASClientV2, self).__init__(*args, **kwargs) def verify_ticket(self, ticket): """Verifies CAS 2.0+/3.0+ XML-based authentication ticket and returns extended attributes""" (response, charset) = self.get_verification_response(ticket) return self.verify_response(response, charset) def get_verification_response(self, ticket): params = [('ticket', ticket), ('service', self.service_url)] if self.renew: params.append(('renew', 'true')) if self.proxy_callback: params.append(('pgtUrl', self.proxy_callback)) base_url = urllib_parse.urljoin(self.server_url, self.url_suffix) url = base_url + '?' + urllib_parse.urlencode(params) page = urllib_request.urlopen(url) try: charset = self.get_page_charset(page) return (page.read(), charset) finally: page.close() @classmethod def parse_attributes_xml_element(cls, element, charset): attributes = dict() for attribute in element: tag = cls.u(attribute.tag, charset).split(u"}").pop() if tag in attributes: if isinstance(attributes[tag], list): attributes[tag].append(cls.u(attribute.text, charset)) else: attributes[tag] = [attributes[tag]] attributes[tag].append(cls.u(attribute.text, charset)) else: if tag == u'attraStyle': pass else: attributes[tag] = cls.u(attribute.text, charset) return attributes @classmethod def verify_response(cls, response, charset): user, attributes, pgtiou = cls.parse_response_xml(response, charset) if len(attributes) == 0: attributes = None return user, attributes, pgtiou @classmethod def parse_response_xml(cls, response, charset): try: from xml.etree import ElementTree except ImportError: from elementtree import ElementTree user = None attributes = {} pgtiou = None tree = ElementTree.fromstring(response) if tree[0].tag.endswith('authenticationSuccess'): for element in tree[0]: if element.tag.endswith('user'): user = cls.u(element.text, charset) elif element.tag.endswith('proxyGrantingTicket'): pgtiou = cls.u(element.text, charset) elif element.tag.endswith('attributes'): attributes = cls.parse_attributes_xml_element(element, charset) return user, attributes, pgtiou class CASClientV3(CASClientV2, SingleLogoutMixin): """CAS Client Version 3""" url_suffix = 'serviceValidate' logout_redirect_param_name = 'service' @classmethod def parse_attributes_xml_element(cls, element, charset): attributes = dict() for attribute in element: tag = cls.u(attribute.tag, charset).split(u"}").pop() if tag in attributes: if isinstance(attributes[tag], list): attributes[tag].append(cls.u(attribute.text, charset)) else: attributes[tag] = [attributes[tag]] attributes[tag].append(cls.u(attribute.text, charset)) else: attributes[tag] = cls.u(attribute.text, charset) return attributes @classmethod def verify_response(cls, response, charset): return cls.parse_response_xml(response, charset) SAML_1_0_NS = 'urn:oasis:names:tc:SAML:1.0:' SAML_1_0_PROTOCOL_NS = '{' + SAML_1_0_NS + 'protocol' + '}' SAML_1_0_ASSERTION_NS = '{' + SAML_1_0_NS + 'assertion' + '}' SAML_ASSERTION_TEMPLATE = """ {ticket} """ class CASClientWithSAMLV1(CASClientV2, SingleLogoutMixin): """CASClient 3.0+ with SAML""" def verify_ticket(self, ticket, **kwargs): """Verifies CAS 3.0+ XML-based authentication ticket and returns extended attributes. @date: 2011-11-30 @author: Carlos Gonzalez Vila Returns username and attributes on success and None,None on failure. """ try: from xml.etree import ElementTree except ImportError: from elementtree import ElementTree page = self.fetch_saml_validation(ticket) charset = self.get_page_charset(page) try: user = None attributes = {} response = page.read() tree = ElementTree.fromstring(response) # Find the authentication status success = tree.find('.//' + SAML_1_0_PROTOCOL_NS + 'StatusCode') if success is not None and success.attrib['Value'].endswith(':Success'): # User is validated name_identifier = tree.find('.//' + SAML_1_0_ASSERTION_NS + 'NameIdentifier') if name_identifier is not None: user = self.u(name_identifier.text, charset) attrs = tree.findall('.//' + SAML_1_0_ASSERTION_NS + 'Attribute') for at in attrs: if self.username_attribute in list(at.attrib.values()): user = self.u( at.find(SAML_1_0_ASSERTION_NS + 'AttributeValue').text, charset ) attributes[u'uid'] = user values = at.findall(SAML_1_0_ASSERTION_NS + 'AttributeValue') key = self.u(at.attrib['AttributeName'], charset) if len(values) > 1: values_array = [] for v in values: values_array.append(self.u(v.text, charset)) attributes[key] = values_array else: attributes[key] = self.u(values[0].text, charset) return user, attributes, None finally: page.close() def fetch_saml_validation(self, ticket): # We do the SAML validation headers = { 'soapaction': 'http://www.oasis-open.org/committees/security', 'cache-control': 'no-cache', 'pragma': 'no-cache', 'accept': 'text/xml', 'connection': 'keep-alive', 'content-type': 'text/xml; charset=utf-8', } params = [('TARGET', self.service_url)] saml_validate_url = urllib_parse.urljoin( self.server_url, 'samlValidate', ) request = Request( saml_validate_url + '?' + urllib_parse.urlencode(params), self.get_saml_assertion(ticket), headers, ) return urllib_request.urlopen(request) @classmethod def get_saml_assertion(cls, ticket): """ http://www.jasig.org/cas/protocol#samlvalidate-cas-3.0 SAML request values: RequestID [REQUIRED]: unique identifier for the request IssueInstant [REQUIRED]: timestamp of the request samlp:AssertionArtifact [REQUIRED]: the valid CAS Service Ticket obtained as a response parameter at login. """ # RequestID [REQUIRED] - unique identifier for the request request_id = uuid4() # e.g. 2014-06-02T09:21:03.071189 timestamp = datetime.datetime.now().isoformat() return SAML_ASSERTION_TEMPLATE.format( request_id=request_id, timestamp=timestamp, ticket=ticket, ).encode('utf8') django-cas-server-1.1.0/cas_server/urls.py0000644000175000017500000000427413416345433022215 0ustar valentinvalentin00000000000000# -*- coding: utf-8 -*- # 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 General Public License version 3 for # more details. # # You should have received a copy of the GNU General Public License version 3 # along with this program; if not, write to the Free Software Foundation, Inc., 51 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # (c) 2015-2016 Valentin Samir """urls for the app""" from django.conf.urls import url from django.views.generic import RedirectView from django.views.decorators.debug import sensitive_post_parameters, sensitive_variables from cas_server import views app_name = "cas_server" urlpatterns = [ url( r'^$', RedirectView.as_view(pattern_name="cas_server:login", permanent=False, query_string=True) ), url( '^login$', sensitive_post_parameters('password')( views.LoginView.as_view() ), name='login' ), url('^logout$', views.LogoutView.as_view(), name='logout'), url('^validate$', views.Validate.as_view(), name='validate'), url( '^serviceValidate$', views.ValidateService.as_view(allow_proxy_ticket=False), name='serviceValidate' ), url( '^proxyValidate$', views.ValidateService.as_view(allow_proxy_ticket=True), name='proxyValidate' ), url('^proxy$', views.Proxy.as_view(), name='proxy'), url( '^p3/serviceValidate$', views.ValidateService.as_view(allow_proxy_ticket=False), name='p3_serviceValidate' ), url( '^p3/proxyValidate$', views.ValidateService.as_view(allow_proxy_ticket=True), name='p3_proxyValidate' ), url('^samlValidate$', views.SamlValidate.as_view(), name='samlValidate'), url( '^auth$', sensitive_variables('password', 'secret')( sensitive_post_parameters('password', 'secret')( views.Auth.as_view() ) ), name='auth' ), url("^federate(?:/(?P([^/]+)))?$", views.FederateAuth.as_view(), name='federateAuth'), ] django-cas-server-1.1.0/cas_server/tests/0000755000175000017500000000000013436457571022022 5ustar valentinvalentin00000000000000django-cas-server-1.1.0/cas_server/tests/utils.py0000644000175000017500000003042413416345433023526 0ustar valentinvalentin00000000000000# -*- coding: utf-8 -*- # 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 General Public License version 3 for # more details. # # You should have received a copy of the GNU General Public License version 3 # along with this program; if not, write to the Free Software Foundation, Inc., 51 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # (c) 2016 Valentin Samir """Some utils functions for tests""" from cas_server.default_settings import settings import django from django.test import Client from django.template import loader from django.utils import timezone import cgi import six from threading import Thread from lxml import etree from six.moves import BaseHTTPServer from six.moves.urllib.parse import urlparse, parse_qsl, parse_qs from datetime import timedelta from cas_server import models from cas_server import utils if django.VERSION < (1, 8): from django.template import Context else: def Context(arg): """ Starting from django 1.8 render take a dict and deprecated the use of a Context. So this is the identity function, only use for compatibility with django 1.7 where render MUST take a Context as argument. """ return arg def return_unicode(string, charset): """make `string` a unicode if `string` is a unicode or bytes encoded with `charset`""" if not isinstance(string, six.text_type): return string.decode(charset) else: return string def return_bytes(string, charset): """ make `string` a bytes encoded with `charset` if `string` is a unicode or bytes encoded with `charset` """ if isinstance(string, six.text_type): return string.encode(charset) else: return string def copy_form(form): """Copy form value into a dict""" params = {} for field in form: if field.value(): params[field.name] = field.value() else: params[field.name] = "" return params def get_login_page_params(client=None): """Return a client and the POST params for the client to login""" if client is None: client = Client() response = client.get('/login') params = copy_form(response.context["form"]) return client, params def get_auth_client(**update): """return a authenticated client""" client, params = get_login_page_params() params["username"] = settings.CAS_TEST_USER params["password"] = settings.CAS_TEST_PASSWORD params.update(update) response = client.post('/login', params) assert client.session.get("authenticated") if params.get("service"): return (client, response) else: return client def get_user_ticket_request(service): """Make an auth client to request a ticket for `service`, return the tuple (user, ticket)""" client = get_auth_client() response = client.get("/login", {"service": service}) ticket_value = response['Location'].split('ticket=')[-1] user = models.User.objects.get( username=settings.CAS_TEST_USER, session_key=client.session.session_key ) ticket = models.ServiceTicket.objects.get(value=ticket_value) return (user, ticket, client) def get_validated_ticket(service): """Return a tick that has being already validated. Used to test SLO""" (ticket, auth_client) = get_user_ticket_request(service)[1:3] client = Client() response = client.get('/validate', {'ticket': ticket.value, 'service': service}) assert response.status_code == 200 assert response.content == b'yes\ntest\n' ticket = models.ServiceTicket.objects.get(value=ticket.value) return (auth_client, ticket) def get_pgt(): """return a dict contening a service, user and PGT ticket for this service""" (httpd, host, port) = HttpParamsHandler.run()[0:3] service = "http://%s:%s" % (host, port) (user, ticket) = get_user_ticket_request(service)[:2] client = Client() client.get('/serviceValidate', {'ticket': ticket.value, 'service': service, 'pgtUrl': service}) params = httpd.PARAMS params["service"] = service params["user"] = user return params def get_proxy_ticket(service): """Return a ProxyTicket waiting for validation""" params = get_pgt() # get a proxy ticket client = Client() response = client.get('/proxy', {'pgt': params['pgtId'], 'targetService': service}) root = etree.fromstring(response.content) proxy_ticket = root.xpath( "//cas:proxyTicket", namespaces={'cas': "http://www.yale.edu/tp/cas"} ) proxy_ticket = proxy_ticket[0].text ticket = models.ProxyTicket.objects.get(value=proxy_ticket) return ticket class HttpParamsHandler(BaseHTTPServer.BaseHTTPRequestHandler): """ A simple http server that return 200 on GET or POST and store GET or POST parameters. Used in unit tests """ def do_GET(self): """Called on a GET request on the BaseHTTPServer""" self.send_response(200) self.send_header(b"Content-type", "text/plain") self.end_headers() self.wfile.write(b"ok") url = urlparse(self.path) params = dict(parse_qsl(url.query)) self.server.PARAMS = params def do_POST(self): """Called on a POST request on the BaseHTTPServer""" ctype, pdict = cgi.parse_header(self.headers.get('content-type')) if ctype == 'multipart/form-data': postvars = cgi.parse_multipart(self.rfile, pdict) elif ctype == 'application/x-www-form-urlencoded': length = int(self.headers.get('content-length')) postvars = parse_qs(self.rfile.read(length), keep_blank_values=1) else: postvars = {} self.server.PARAMS = postvars def log_message(self, *args): """silent any log message""" return @classmethod def run(cls, port=0): """Run a BaseHTTPServer using this class as handler""" server_class = BaseHTTPServer.HTTPServer httpd = server_class(("127.0.0.1", port), cls) (host, port) = httpd.socket.getsockname() def lauch(): """routine to lauch in a background thread""" httpd.handle_request() httpd.server_close() httpd_thread = Thread(target=lauch) httpd_thread.daemon = True httpd_thread.start() return (httpd, host, port) class Http404Handler(HttpParamsHandler): """A simple http server that always return 404 not found. Used in unit tests""" def do_GET(self): """Called on a GET request on the BaseHTTPServer""" self.send_response(404) self.send_header(b"Content-type", "text/plain") self.end_headers() self.wfile.write(b"error 404 not found") def do_POST(self): """Called on a POST request on the BaseHTTPServer""" return self.do_GET() class DummyCAS(BaseHTTPServer.BaseHTTPRequestHandler): """A dummy CAS that validate for only one (service, ticket) used in federated mode tests""" #: dict of the last receive GET parameters params = None def test_params(self): """check that internal and provided (service, ticket) matches""" if ( self.server.ticket is not None and self.params.get("service").encode("ascii") == self.server.service and self.params.get("ticket").encode("ascii") == self.server.ticket ): self.server.ticket = None return True else: return False def send_headers(self, code, content_type): """send http headers""" self.send_response(code) self.send_header("Content-type", content_type) self.end_headers() def do_GET(self): """Called on a GET request on the BaseHTTPServer""" url = urlparse(self.path) self.params = dict(parse_qsl(url.query)) if url.path == "/validate": self.send_headers(200, "text/plain; charset=utf-8") if self.test_params(): self.wfile.write(b"yes\n" + self.server.username + b"\n") self.server.ticket = None else: self.wfile.write(b"no\n") elif url.path in { '/serviceValidate', '/serviceValidate', '/p3/serviceValidate', '/p3/proxyValidate' }: self.send_headers(200, "text/xml; charset=utf-8") if self.test_params(): template = loader.get_template('cas_server/serviceValidate.xml') context = Context({ 'username': self.server.username.decode('utf-8'), 'attributes': self.server.attributes, 'auth_date': timezone.now().replace(microsecond=0).isoformat(), 'is_new_login': 'true', }) self.wfile.write(return_bytes(template.render(context), "utf8")) else: template = loader.get_template('cas_server/serviceValidateError.xml') context = Context({ 'code': 'BAD_SERVICE_TICKET', 'msg': 'Valids are (%r, %r)' % (self.server.service, self.server.ticket) }) self.wfile.write(return_bytes(template.render(context), "utf8")) else: self.return_404() def do_POST(self): """Called on a POST request on the BaseHTTPServer""" url = urlparse(self.path) self.params = dict(parse_qsl(url.query)) if url.path == "/samlValidate": self.send_headers(200, "text/xml; charset=utf-8") length = int(self.headers.get('content-length')) root = etree.fromstring(self.rfile.read(length)) auth_req = root.getchildren()[1].getchildren()[0] ticket = auth_req.getchildren()[0].text.encode("ascii") if ( self.server.ticket is not None and self.params.get("TARGET").encode("ascii") == self.server.service and ticket == self.server.ticket ): self.server.ticket = None template = loader.get_template('cas_server/samlValidate.xml') context = Context({ 'IssueInstant': timezone.now().isoformat(), 'expireInstant': (timezone.now() + timedelta(seconds=60)).isoformat(), 'Recipient': self.server.service, 'ResponseID': utils.gen_saml_id(), 'username': self.server.username.decode('utf-8'), 'attributes': self.server.attributes, 'auth_date': timezone.now().replace(microsecond=0).isoformat(), 'is_new_login': 'true', }) self.wfile.write(return_bytes(template.render(context), "utf8")) else: template = loader.get_template('cas_server/samlValidateError.xml') context = Context({ 'IssueInstant': timezone.now().isoformat(), 'ResponseID': utils.gen_saml_id(), 'code': 'BAD_SERVICE_TICKET', 'msg': 'Valids are (%r, %r)' % (self.server.service, self.server.ticket) }) self.wfile.write(return_bytes(template.render(context), "utf8")) else: self.return_404() def return_404(self): """return a 404 error""" self.send_headers(404, "text/plain; charset=utf-8") self.wfile.write("not found") def log_message(self, *args): """silent any log message""" return @classmethod def run(cls, service, ticket, username, attributes, port=0): """Run a BaseHTTPServer using this class as handler""" server_class = BaseHTTPServer.HTTPServer httpd = server_class(("127.0.0.1", port), cls) httpd.service = service httpd.ticket = ticket httpd.username = username httpd.attributes = attributes (host, port) = httpd.socket.getsockname() def lauch(): """routine to lauch in a background thread""" httpd.handle_request() httpd.server_close() httpd_thread = Thread(target=lauch) httpd_thread.daemon = True httpd_thread.start() return (httpd, host, port) django-cas-server-1.1.0/cas_server/tests/__init__.py0000644000175000017500000000000012757022243024106 0ustar valentinvalentin00000000000000django-cas-server-1.1.0/cas_server/tests/test_federate.py0000644000175000017500000005161213271404255025203 0ustar valentinvalentin00000000000000# -*- coding: utf-8 -*- # 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 General Public License version 3 for # more details. # # You should have received a copy of the GNU General Public License version 3 # along with this program; if not, write to the Free Software Foundation, Inc., 51 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # (c) 2016 Valentin Samir """tests for the CAS federate mode""" from cas_server import default_settings from cas_server.default_settings import settings import django from django.test import TestCase, Client from django.test.utils import override_settings from six.moves import reload_module from cas_server import utils, models from cas_server.tests.mixin import BaseServicePattern, CanLogin, FederatedIendityProviderModel from cas_server.tests import utils as tests_utils PROVIDERS = { "example.com": ("http://127.0.0.1:8080", '1', "Example dot com"), "example.org": ("http://127.0.0.1:8081", '2', "Example dot org"), "example.net": ("http://127.0.0.1:8082", '3', "Example dot net"), "example.test": ("http://127.0.0.1:8083", 'CAS_2_SAML_1_0', 'Example fot test'), } @override_settings( CAS_FEDERATE=True, CAS_AUTH_CLASS="cas_server.auth.CASFederateAuth", # test with a non ascii username CAS_TEST_USER=u"dédé" ) class FederateAuthLoginLogoutTestCase( TestCase, BaseServicePattern, CanLogin, FederatedIendityProviderModel ): """tests for the views login logout and federate then the federated mode is enabled""" def setUp(self): """Prepare the test context""" self.setup_service_patterns() self.setup_federated_identity_provider(PROVIDERS) def test_default_settings(self): """default settings should populated some default variable then CAS_FEDERATE is True""" del settings.CAS_AUTH_CLASS reload_module(default_settings) self.assertEqual(settings.CAS_AUTH_CLASS, "cas_server.auth.CASFederateAuth") def test_login_get_provider(self): """some assertion about the login page in federated mode""" client = Client() response = client.get("/login") self.assertEqual(response.status_code, 200) for provider in models.FederatedIendityProvider.objects.all(): self.assertTrue('' % ( provider.suffix, provider.verbose_name ) in response.content.decode("utf-8")) self.assertEqual(response.context['post_url'], '/federate') def test_login_post_provider(self, remember=False): """test a successful login wrokflow""" tickets = [] # choose the example.com provider for (suffix, cas_port) in [ ("example.com", 8080), ("example.org", 8081), ("example.net", 8082), ("example.test", 8083) ]: provider = models.FederatedIendityProvider.objects.get(suffix=suffix) # get a bare client client = Client() # fetch the login page response = client.get("/login") # in federated mode, we shoudl POST do /federate on the login page self.assertEqual(response.context['post_url'], '/federate') # get current form parameter params = tests_utils.copy_form(response.context["form"]) params['provider'] = provider.suffix if remember: params['remember'] = 'on' # just try for one suffix if suffix == "example.com": # if renew=False is posted it should be ignored params["renew"] = False # post the choosed provider response = client.post('/federate', params) # we are redirected to the provider CAS client url self.assertEqual(response.status_code, 302) self.assertEqual(response["Location"], '%s/federate/%s%s' % ( 'http://testserver' if django.VERSION < (1, 9) else "", provider.suffix, "?remember=on" if remember else "" )) # let's follow the redirect response = client.get( '/federate/%s%s' % (provider.suffix, "?remember=on" if remember else "") ) # we are redirected to the provider CAS for authentication self.assertEqual(response.status_code, 302) self.assertEqual( response["Location"], "%s/login?service=http%%3A%%2F%%2Ftestserver%%2Ffederate%%2F%s%s" % ( provider.server_url, provider.suffix, "%3Fremember%3Don" if remember else "" ) ) # let's generate a ticket ticket = utils.gen_st() # we lauch a dummy CAS server that only validate once for the service # http://testserver/federate/example.com with `ticket` tests_utils.DummyCAS.run( ("http://testserver/federate/%s%s" % ( provider.suffix, "?remember=on" if remember else "" )).encode("ascii"), ticket.encode("ascii"), settings.CAS_TEST_USER.encode("utf8"), [], cas_port ) # we normally provide a good ticket and should be redirected to /login as the ticket # get successfully validated again the dummy CAS response = client.get( '/federate/%s' % provider.suffix, {'ticket': ticket, 'remember': 'on' if remember else ''} ) if remember: self.assertIn("remember_provider", client.cookies) self.assertEqual(client.cookies["remember_provider"].value, provider.suffix) self.assertEqual(response.status_code, 302) self.assertEqual(response["Location"], "%s/login" % ( 'http://testserver' if django.VERSION < (1, 9) else "" )) # follow the redirect response = client.get("/login") # we should get a page with a from with all widget hidden that auto POST to /login using # javascript. If javascript is disabled, a "connect" button is showed self.assertTrue(response.context['auto_submit']) self.assertEqual(response.context['post_url'], '/login') params = tests_utils.copy_form(response.context["form"]) # POST ge prefiled from parameters response = client.post("/login", params) # the user should now being authenticated using username test@`provider` self.assert_logged( client, response, username=provider.build_username(settings.CAS_TEST_USER) ) tickets.append((provider, ticket, client)) # try to get a ticket response = client.get("/login", {'service': self.service}) self.assertEqual(response.status_code, 302) self.assertTrue(response["Location"].startswith("%s?ticket=" % self.service)) return tickets def test_login_twice(self): """Test that user id db is used for the second login (cf coverage)""" self.test_login_post_provider() tickets = self.test_login_post_provider() # trying to authenticated while being already authenticated should redirect to /login for (provider, _, client) in tickets: response = client.get("/federate/%s" % provider.suffix) self.assertEqual(response.status_code, 302) self.assertEqual(response["Location"], "%s/login" % ( 'http://testserver' if django.VERSION < (1, 9) else "" )) @override_settings(CAS_FEDERATE=False) def test_auth_federate_false(self): """federated view should redirect to /login then CAS_FEDERATE is False""" provider = "example.com" client = Client() response = client.get("/federate/%s" % provider) self.assertEqual(response.status_code, 302) self.assertEqual(response["Location"], "%s/login" % ( 'http://testserver' if django.VERSION < (1, 9) else "" )) response = client.post("%s/federate/%s" % ( 'http://testserver' if django.VERSION < (1, 9) else "", provider )) self.assertEqual(response.status_code, 302) self.assertEqual(response["Location"], "%s/login" % ( 'http://testserver' if django.VERSION < (1, 9) else "" )) def test_auth_federate_errors(self): """ The federated view should redirect to /login if the provider is unknown or not provided, try to fetch a new ticket if the provided ticket validation fail (network error or bad ticket), redirect to /login with a error message if identity provider CAS return a bad response (invalid XML document) """ good_provider = "example.com" bad_provider = "exemple.fr" client = Client() response = client.get("/federate/%s" % bad_provider) self.assertEqual(response.status_code, 302) self.assertEqual(response["Location"], "%s/login" % ( 'http://testserver' if django.VERSION < (1, 9) else "" )) # test CAS not avaible response = client.get("/federate/%s" % good_provider, {'ticket': utils.gen_st()}) self.assertEqual(response.status_code, 302) self.assertEqual( response["Location"], "%s/login?service=http%%3A%%2F%%2Ftestserver%%2Ffederate%%2F%s" % ( models.FederatedIendityProvider.objects.get(suffix=good_provider).server_url, good_provider ) ) # test CAS avaible but bad ticket tests_utils.DummyCAS.run( ("http://testserver/federate/%s" % good_provider).encode("ascii"), utils.gen_st().encode("ascii"), settings.CAS_TEST_USER.encode("utf-8"), [], 8080 ) response = client.get("/federate/%s" % good_provider, {'ticket': utils.gen_st()}) self.assertEqual(response.status_code, 302) self.assertEqual( response["Location"], "%s/login?service=http%%3A%%2F%%2Ftestserver%%2Ffederate%%2F%s" % ( models.FederatedIendityProvider.objects.get(suffix=good_provider).server_url, good_provider ) ) response = client.post("/federate") self.assertEqual(response.status_code, 302) self.assertEqual(response["Location"], "%s/login" % ( 'http://testserver' if django.VERSION < (1, 9) else "" )) # test CAS avaible but return a bad XML doc, should redirect to /login with a error message # use "example.net" as it is CASv3 tests_utils.HttpParamsHandler.run(8082) response = client.get("/federate/%s" % "example.net", {'ticket': utils.gen_st()}) self.assertEqual(response.status_code, 302) self.assertEqual(response["Location"], "%s/login" % ( 'http://testserver' if django.VERSION < (1, 9) else "" )) response = client.get("/login") self.assertEqual(response.status_code, 200) self.assertIn(b"Invalid response from your identity provider CAS", response.content) def test_auth_federate_slo(self): """test that SLO receive from backend CAS log out the users""" # get tickets and connected clients tickets = self.test_login_post_provider() for (provider, ticket, client) in tickets: # SLO for an unkown ticket should do nothing response = client.post( "/federate/%s" % provider.suffix, {'logoutRequest': utils.logout_request(utils.gen_st())} ) self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b"ok") # Bad SLO format should do nothing response = client.post( "/federate/%s" % provider.suffix, {'logoutRequest': ""} ) self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b"ok") # Bad SLO format should do nothing response = client.post( "/federate/%s" % provider.suffix, {'logoutRequest': ""} ) self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b"ok") response = client.get("/login") self.assert_logged( client, response, username=provider.build_username(settings.CAS_TEST_USER) ) # SLO for a previously logged ticket should log out the user if CAS version is # 3 or 'CAS_2_SAML_1_0' response = client.post( "/federate/%s" % provider.suffix, {'logoutRequest': utils.logout_request(ticket)} ) self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b"ok") response = client.get("/login") if provider.cas_protocol_version in {'3', 'CAS_2_SAML_1_0'}: # support SLO self.assert_login_failed(client, response) else: self.assert_logged( client, response, username=provider.build_username(settings.CAS_TEST_USER) ) def test_federate_logout(self): """ test the logout function: the user should be log out and redirected to his CAS logout page """ # get tickets and connected clients, then follow normal logout tickets = self.test_login_post_provider() for (provider, _, client) in tickets: response = client.get("/logout") self.assertEqual(response.status_code, 302) self.assertEqual( response["Location"], "%s/logout" % provider.server_url, ) response = client.get("/login") self.assert_login_failed(client, response) # test if the user is already logged out response = client.get("/logout") # no redirection self.assertEqual(response.status_code, 200) self.assertTrue( ( b"You were already logged out from the Central Authentication Service." ) in response.content ) tickets = self.test_login_post_provider() if django.VERSION >= (1, 8): # assume the username session variable has been tempered (should not happend) for (provider, _, client) in tickets: session = client.session session["username"] = settings.CAS_TEST_USER session.save() response = client.get("/logout") self.assertEqual(response.status_code, 200) response = client.get("/login") self.assert_login_failed(client, response) def test_remember_provider(self): """ If the user check remember, next login should not offer the chose of the backend CAS and use the one store in the cookie """ tickets = self.test_login_post_provider(remember=True) for (provider, _, client) in tickets: client.get("/logout") response = client.get("/login") self.assertEqual(response.status_code, 302) self.assertEqual(response["Location"], "%s/federate/%s" % ( 'http://testserver' if django.VERSION < (1, 9) else "", provider.suffix )) def test_forget_provider(self): """Test the logout option to forget remembered provider""" tickets = self.test_login_post_provider(remember=True) for (provider, _, client) in tickets: self.assertIn("remember_provider", client.cookies) self.assertEqual(client.cookies["remember_provider"].value, provider.suffix) self.assertNotEqual(client.cookies["remember_provider"]["max-age"], 0) client.get("/logout?forget_provider=1") self.assertEqual(client.cookies["remember_provider"]["max-age"], 0) def test_renew(self): """ Test authentication renewal with federation mode """ tickets = self.test_login_post_provider() for (provider, _, client) in tickets: # Try to renew authentication(client already authenticated in test_login_post_provider response = client.get("/login?renew=true") # we should be redirected to the user CAS self.assertEqual(response.status_code, 302) self.assertEqual(response["Location"], "%s/federate/%s?renew=true" % ( 'http://testserver' if django.VERSION < (1, 9) else "", provider.suffix )) response = client.get("/federate/%s?renew=true" % provider.suffix) self.assertEqual(response.status_code, 302) service_url = ( "service=http%%3A%%2F%%2Ftestserver%%2Ffederate%%2F%s%%3Frenew%%3Dtrue" ) % provider.suffix self.assertIn(service_url, response["Location"]) self.assertIn("renew=true", response["Location"]) cas_port = int(provider.server_url.split(':')[-1]) # let's generate a ticket ticket = utils.gen_st() # we lauch a dummy CAS server that only validate once for the service # http://testserver/federate/example.com?renew=true with `ticket` tests_utils.DummyCAS.run( ("http://testserver/federate/%s?renew=true" % provider.suffix).encode("ascii"), ticket.encode("ascii"), settings.CAS_TEST_USER.encode("utf8"), [], cas_port ) # we normally provide a good ticket and should be redirected to /login as the ticket # get successfully validated again the dummy CAS response = client.get( '/federate/%s' % provider.suffix, {'ticket': ticket, 'renew': 'true'} ) self.assertEqual(response.status_code, 302) self.assertEqual(response["Location"], "%s/login?renew=true" % ( 'http://testserver' if django.VERSION < (1, 9) else "" )) # follow the redirect and try to get a ticket to see is it has renew set to True response = client.get("/login?renew=true&service=%s" % self.service) # we should get a page with a from with all widget hidden that auto POST to /login using # javascript. If javascript is disabled, a "connect" button is showed self.assertTrue(response.context['auto_submit']) self.assertEqual(response.context['post_url'], '/login') params = tests_utils.copy_form(response.context["form"]) # POST get prefiled from parameters response = client.post("/login", params) self.assertEqual(response.status_code, 302) self.assertTrue(response["Location"].startswith("%s?ticket=" % self.service)) ticket_value = response["Location"].split('ticket=')[-1] ticket = models.ServiceTicket.objects.get(value=ticket_value) self.assertTrue(ticket.renew) def test_login_bad_ticket(self): """ Try login with a bad ticket: login should fail and the main login page should be displayed to the user """ provider = "example.com" # get a bare client client = Client() session = client.session session["federate_username"] = models.FederatedIendityProvider.build_username_from_suffix( settings.CAS_TEST_USER, provider ) session["federate_ticket"] = utils.gen_st() if django.VERSION >= (1, 8): session.save() response = client.get("/login") # we should get a page with a from with all widget hidden that auto POST to /login using # javascript. If javascript is disabled, a "connect" button is showed self.assertTrue(response.context['auto_submit']) self.assertEqual(response.context['post_url'], '/login') params = tests_utils.copy_form(response.context["form"]) # POST, as (username, ticket) are not valid, we should get the federate login page response = client.post("/login", params) self.assertEqual(response.status_code, 200) for provider in models.FederatedIendityProvider.objects.all(): self.assertIn( '' % ( provider.suffix, provider.verbose_name ), response.content.decode("utf-8") ) self.assertEqual(response.context['post_url'], '/federate') django-cas-server-1.1.0/cas_server/tests/mixin.py0000644000175000017500000002540413416345433023514 0ustar valentinvalentin00000000000000# -*- coding: utf-8 -*- # 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 General Public License version 3 for # more details. # # You should have received a copy of the GNU General Public License version 3 # along with this program; if not, write to the Free Software Foundation, Inc., 51 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # (c) 2016 Valentin Samir """Some mixin classes for tests""" from cas_server.default_settings import settings from django.utils import timezone import re from lxml import etree from datetime import timedelta from cas_server import models from cas_server.tests.utils import get_auth_client class BaseServicePattern(object): """Mixing for setting up service pattern for testing""" @classmethod def setup_service_patterns(cls, proxy=False): """setting up service pattern""" # For general purpose testing cls.service = "https://www.example.com" cls.service_pattern = models.ServicePattern.objects.create( name="example", pattern=r"^https://www\.example\.com(/.*)?$", proxy=proxy, ) models.ReplaceAttributName.objects.create(name="*", service_pattern=cls.service_pattern) # For testing the restrict_users attributes cls.service_restrict_user_fail = "https://restrict_user_fail.example.com" cls.service_pattern_restrict_user_fail = models.ServicePattern.objects.create( name="restrict_user_fail", pattern=r"^https://restrict_user_fail\.example\.com(/.*)?$", restrict_users=True, proxy=proxy, ) cls.service_restrict_user_success = "https://restrict_user_success.example.com" cls.service_pattern_restrict_user_success = models.ServicePattern.objects.create( name="restrict_user_success", pattern=r"^https://restrict_user_success\.example\.com(/.*)?$", restrict_users=True, proxy=proxy, ) models.Username.objects.create( value=settings.CAS_TEST_USER, service_pattern=cls.service_pattern_restrict_user_success ) # For testing the user attributes filtering conditions cls.service_filter_fail = "https://filter_fail.example.com" cls.service_pattern_filter_fail = models.ServicePattern.objects.create( name="filter_fail", pattern=r"^https://filter_fail\.example\.com(/.*)?$", proxy=proxy, ) models.FilterAttributValue.objects.create( attribut="right", pattern="^admin$", service_pattern=cls.service_pattern_filter_fail ) cls.service_filter_fail_alt = "https://filter_fail_alt.example.com" cls.service_pattern_filter_fail_alt = models.ServicePattern.objects.create( name="filter_fail_alt", pattern=r"^https://filter_fail_alt\.example\.com(/.*)?$", proxy=proxy, ) models.FilterAttributValue.objects.create( attribut="nom", pattern="^toto$", service_pattern=cls.service_pattern_filter_fail_alt ) cls.service_filter_success = "https://filter_success.example.com" cls.service_pattern_filter_success = models.ServicePattern.objects.create( name="filter_success", pattern=r"^https://filter_success\.example\.com(/.*)?$", proxy=proxy, ) models.FilterAttributValue.objects.create( attribut="email", pattern="^%s$" % re.escape(settings.CAS_TEST_ATTRIBUTES['email']), service_pattern=cls.service_pattern_filter_success ) # For testing the user_field attributes cls.service_field_needed_fail = "https://field_needed_fail.example.com" cls.service_pattern_field_needed_fail = models.ServicePattern.objects.create( name="field_needed_fail", pattern=r"^https://field_needed_fail\.example\.com(/.*)?$", user_field="uid", proxy=proxy, ) cls.service_field_needed_success = "https://field_needed_success.example.com" cls.service_pattern_field_needed_success = models.ServicePattern.objects.create( name="field_needed_success", pattern=r"^https://field_needed_success\.example\.com(/.*)?$", user_field="alias", proxy=proxy, ) cls.service_field_needed_success_alt = "https://field_needed_success_alt.example.com" cls.service_pattern_field_needed_success = models.ServicePattern.objects.create( name="field_needed_success_alt", pattern=r"^https://field_needed_success_alt\.example\.com(/.*)?$", user_field="nom", proxy=proxy, ) class XmlContent(object): """Mixin for test on CAS XML responses""" def assert_error(self, response, code, text=None): """Assert a validation error""" self.assertEqual(response.status_code, 200) root = etree.fromstring(response.content) error = root.xpath( "//cas:authenticationFailure", namespaces={'cas': "http://www.yale.edu/tp/cas"} ) self.assertEqual(len(error), 1) self.assertEqual(error[0].attrib['code'], code) if text is not None: self.assertEqual(error[0].text, text) def assert_success(self, response, username, original_attributes): """assert a ticket validation success""" self.assertEqual(response.status_code, 200) root = etree.fromstring(response.content) sucess = root.xpath( "//cas:authenticationSuccess", namespaces={'cas': "http://www.yale.edu/tp/cas"} ) self.assertTrue(sucess) users = root.xpath("//cas:user", namespaces={'cas': "http://www.yale.edu/tp/cas"}) self.assertEqual(len(users), 1) self.assertEqual(users[0].text, username) attributes = root.xpath( "//cas:attributes", namespaces={'cas': "http://www.yale.edu/tp/cas"} ) self.assertEqual(len(attributes), 1) ignore_attrs = { "authenticationDate", "longTermAuthenticationRequestTokenUsed", "isFromNewLogin" } ignored_attrs = 0 attrs1 = set() for attr in attributes[0]: name = attr.tag[len("http://www.yale.edu/tp/cas")+2:] if name not in ignore_attrs: attrs1.add((name, attr.text)) else: ignored_attrs += 1 attributes = root.xpath("//cas:attribute", namespaces={'cas': "http://www.yale.edu/tp/cas"}) self.assertEqual(len(attributes), len(attrs1) + ignored_attrs) attrs2 = set() for attr in attributes: name = attr.attrib['name'] if name not in ignore_attrs: attrs2.add((name, attr.attrib['value'])) original = set() for key, value in original_attributes.items(): if isinstance(value, list): for sub_value in value: original.add((key, sub_value)) else: original.add((key, value)) self.assertEqual(attrs1, attrs2) self.assertEqual(attrs1, original) return root class UserModels(object): """Mixin for test on CAS user models""" @staticmethod def expire_user(): """return an expired user""" client = get_auth_client() new_date = timezone.now() - timedelta(seconds=(settings.SESSION_COOKIE_AGE + 600)) models.User.objects.filter( username=settings.CAS_TEST_USER, session_key=client.session.session_key ).update(date=new_date) return client @staticmethod def tgt_expired_user(sec): """return a user logged since sec seconds""" client = get_auth_client() new_date = timezone.now() - timedelta(seconds=(sec)) models.User.objects.filter( username=settings.CAS_TEST_USER, session_key=client.session.session_key ).update(last_login=new_date) return client @staticmethod def get_user(client): """return the user associated with an authenticated client""" return models.User.objects.get( username=settings.CAS_TEST_USER, session_key=client.session.session_key ) class CanLogin(object): """Assertion about login""" def assert_logged( self, client, response, warn=False, code=200, username=settings.CAS_TEST_USER ): """Assertions testing that client is well authenticated""" self.assertEqual(response.status_code, code) # this message is displayed to the user upon successful authentication self.assertIn( ( b"You have successfully logged into " b"the Central Authentication Service" ), response.content ) # these session variables a set if usccessfully authenticated self.assertEqual(client.session["username"], username) self.assertIs(client.session["warn"], warn) self.assertIs(client.session["authenticated"], True) # on successfull authentication, a corresponding user object is created self.assertTrue( models.User.objects.get( username=username, session_key=client.session.session_key ) ) def assert_login_failed(self, client, response, code=200): """Assertions testing a failed login attempt""" self.assertEqual(response.status_code, code) # this message is displayed to the user upon successful authentication, so it should not # appear self.assertNotIn( ( b"You have successfully logged into " b"the Central Authentication Service" ), response.content ) # if authentication has failed, these session variables should not be set self.assertTrue(client.session.get("username") is None) self.assertTrue(client.session.get("warn") is None) self.assertTrue(client.session.get("authenticated") is None) class FederatedIendityProviderModel(object): """Mixin for test classes using the FederatedIendityProvider model""" @staticmethod def setup_federated_identity_provider(providers): """setting up federated identity providers""" for suffix, (server_url, cas_protocol_version, verbose_name) in providers.items(): models.FederatedIendityProvider.objects.create( suffix=suffix, server_url=server_url, cas_protocol_version=cas_protocol_version, verbose_name=verbose_name ) django-cas-server-1.1.0/cas_server/tests/urls.py0000644000175000017500000000151412757022243023347 0ustar valentinvalentin00000000000000"""cas URL Configuration The `urlpatterns` list routes URLs to views. For more information please see: https://docs.djangoproject.com/en/1.9/topics/http/urls/ Examples: Function views 1. Add an import: from my_app import views 2. Add a URL to urlpatterns: url(r'^$', views.home, name='home') Class-based views 1. Add an import: from other_app.views import Home 2. Add a URL to urlpatterns: url(r'^$', Home.as_view(), name='home') Including another URLconf 1. Import the include() function: from django.conf.urls import url, include, include 2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls')) """ from django.conf.urls import url, include from django.contrib import admin urlpatterns = [ url(r'^admin/', admin.site.urls), url(r'^', include('cas_server.urls', namespace='cas_server')), ] django-cas-server-1.1.0/cas_server/tests/test_templatetags.py0000644000175000017500000000235712757022243026121 0ustar valentinvalentin00000000000000# -*- coding: utf-8 -*- # 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 General Public License version 3 for # more details. # # You should have received a copy of the GNU General Public License version 3 # along with this program; if not, write to the Free Software Foundation, Inc., 51 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # (c) 2016 Valentin Samir """tests for the customs template tags""" from django.test import TestCase from cas_server import forms from cas_server.templatetags import cas_server class TemplateTagsTestCase(TestCase): """tests for the customs template tags""" def test_is_checkbox(self): """test for the template filter is_checkbox""" form = forms.UserCredential() self.assertFalse(cas_server.is_checkbox(form["username"])) self.assertTrue(cas_server.is_checkbox(form["warn"])) def test_is_hidden(self): """test for the template filter is_hidden""" form = forms.UserCredential() self.assertFalse(cas_server.is_hidden(form["username"])) self.assertTrue(cas_server.is_hidden(form["lt"])) django-cas-server-1.1.0/cas_server/tests/auth.py0000644000175000017500000000216213060003340023304 0ustar valentinvalentin00000000000000# -*- coding: utf-8 -*- # 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 General Public License version 3 for # more details. # # You should have received a copy of the GNU General Public License version 3 # along with this program; if not, write to the Free Software Foundation, Inc., 51 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # (c) 2016 Valentin Samir """Some test authentication classes for the CAS""" from cas_server import auth class TestCachedAttributesAuthUser(auth.TestAuthUser): """ A test authentication class only working for one unique user. :param unicode username: A username, stored in the :attr:`username` class attribute. The uniq valid value is ``settings.CAS_TEST_USER``. """ def attributs(self): """ The user attributes. :raises NotImplementedError: as this class do not support fetching user attributes """ raise NotImplementedError() django-cas-server-1.1.0/cas_server/tests/test_models.py0000644000175000017500000003641613416345433024717 0ustar valentinvalentin00000000000000# -*- coding: utf-8 -*- # 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 General Public License version 3 for # more details. # # You should have received a copy of the GNU General Public License version 3 # along with this program; if not, write to the Free Software Foundation, Inc., 51 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # (c) 2016 Valentin Samir """Tests module for models""" from cas_server.default_settings import settings import django from django.test import TestCase, Client from django.test.utils import override_settings from django.utils import timezone from django.core import mail import mock from datetime import timedelta from importlib import import_module from cas_server import models, utils from cas_server.tests.utils import get_auth_client, HttpParamsHandler from cas_server.tests.mixin import UserModels, BaseServicePattern, FederatedIendityProviderModel from cas_server.tests.test_federate import PROVIDERS SessionStore = import_module(settings.SESSION_ENGINE).SessionStore class FederatedUserTestCase(TestCase, UserModels, FederatedIendityProviderModel): """test for the federated user model""" def setUp(self): """Prepare the test context""" self.setup_federated_identity_provider(PROVIDERS) def test_clean_old_entries(self): """tests for clean_old_entries that should delete federated user no longer used""" client = Client() client.get("/login") provider = models.FederatedIendityProvider.objects.get(suffix="example.com") models.FederatedUser.objects.create( username="test1", provider=provider, attributs={}, ticket="" ) models.FederatedUser.objects.create( username="test2", provider=provider, attributs={}, ticket="" ) models.FederatedUser.objects.all().update( last_update=(timezone.now() - timedelta(seconds=settings.CAS_TICKET_TIMEOUT + 10)) ) models.FederatedUser.objects.create( username="test3", provider=provider, attributs={}, ticket="" ) models.User.objects.create( username="test1@example.com", session_key=client.session.session_key ) self.assertEqual(len(models.FederatedUser.objects.all()), 3) models.FederatedUser.clean_old_entries() self.assertEqual(len(models.FederatedUser.objects.all()), 2) with self.assertRaises(models.FederatedUser.DoesNotExist): models.FederatedUser.objects.get(username="test2") def test_json_attributes(self): """test the json storage of ``atrributs`` in ``_attributs``""" provider = models.FederatedIendityProvider.objects.get(suffix="example.com") user = models.FederatedUser.objects.create( username=settings.CAS_TEST_USER, provider=provider, attributs=settings.CAS_TEST_ATTRIBUTES, ticket="" ) self.assertEqual(utils.json_encode(settings.CAS_TEST_ATTRIBUTES), user._attributs) user.delete() user = models.FederatedUser.objects.create( username=settings.CAS_TEST_USER, provider=provider, ticket="" ) self.assertIsNone(user._attributs) self.assertIsNone(user.attributs) class FederateSLOTestCase(TestCase, UserModels): """test for the federated SLO model""" def test_clean_deleted_sessions(self): """ tests for clean_deleted_sessions that should delete object for which matching session do not exists anymore """ if django.VERSION >= (1, 8): client1 = Client() client2 = Client() client1.get("/login") client2.get("/login") session = client2.session session['authenticated'] = True session.save() models.FederateSLO.objects.create( username="test1@example.com", session_key=client1.session.session_key, ticket=utils.gen_st() ) models.FederateSLO.objects.create( username="test2@example.com", session_key=client2.session.session_key, ticket=utils.gen_st() ) self.assertEqual(len(models.FederateSLO.objects.all()), 2) models.FederateSLO.clean_deleted_sessions() self.assertEqual(len(models.FederateSLO.objects.all()), 1) with self.assertRaises(models.FederateSLO.DoesNotExist): models.FederateSLO.objects.get(username="test1@example.com") @override_settings(CAS_AUTH_CLASS='cas_server.auth.TestAuthUser') class UserAttributesTestCase(TestCase, UserModels): """test for the user attributes cache model""" def test_clean_old_entries(self): """test the clean_old_entries methode""" client = get_auth_client() user = self.get_user(client) models.UserAttributes.objects.create(username=settings.CAS_TEST_USER) # test that attribute cache is removed for non existant users self.assertEqual(len(models.UserAttributes.objects.all()), 1) models.UserAttributes.clean_old_entries() self.assertEqual(len(models.UserAttributes.objects.all()), 1) user.delete() models.UserAttributes.clean_old_entries() self.assertEqual(len(models.UserAttributes.objects.all()), 0) @override_settings(CAS_AUTH_CLASS='cas_server.auth.TestAuthUser') class UserTestCase(TestCase, UserModels): """tests for the user models""" def setUp(self): """Prepare the test context""" self.service = 'http://127.0.0.1:45678' self.service_pattern = models.ServicePattern.objects.create( name="localhost", pattern=r"^https?://127\.0\.0\.1(:[0-9]+)?(/.*)?$", single_log_out=True ) models.ReplaceAttributName.objects.create(name="*", service_pattern=self.service_pattern) def test_clean_old_entries(self): """test clean_old_entries""" # get an authenticated client client = self.expire_user() # assert the user exists before being cleaned self.assertEqual(len(models.User.objects.all()), 1) # assert the last activity date is before the expiry date self.assertTrue( self.get_user(client).date < ( timezone.now() - timedelta(seconds=settings.SESSION_COOKIE_AGE) ) ) # delete old inactive users models.User.clean_old_entries() # assert the user has being well delete self.assertEqual(len(models.User.objects.all()), 0) @override_settings(CAS_TGT_VALIDITY=3600) def test_clean_old_entries_tgt_expired(self): """test clean_old_entiers with CAS_TGT_VALIDITY set""" # get an authenticated client client = self.tgt_expired_user(settings.CAS_TGT_VALIDITY + 60) # assert the user exists before being cleaned self.assertEqual(len(models.User.objects.all()), 1) # assert the last lofin date is before the expiry date self.assertTrue( self.get_user(client).last_login < ( timezone.now() - timedelta(seconds=settings.CAS_TGT_VALIDITY) ) ) # delete old inactive users models.User.clean_old_entries() # assert the user has being well delete self.assertEqual(len(models.User.objects.all()), 0) def test_clean_deleted_sessions(self): """test clean_deleted_sessions""" # get an authenticated client client1 = get_auth_client() client2 = get_auth_client() # generate a ticket to fire SLO during user cleaning (SLO should fail a nothing listen # on self.service) ticket = self.get_user(client1).get_ticket( models.ServiceTicket, self.service, self.service_pattern, renew=False ) ticket.validate = True ticket.save() # simulated expired session being garbage collected for client1 session = SessionStore(session_key=client1.session.session_key) session.flush() # assert the user exists before being cleaned self.assertTrue(self.get_user(client1)) self.assertTrue(self.get_user(client2)) self.assertEqual(len(models.User.objects.all()), 2) # session has being remove so the user of client1 is no longer authenticated self.assertFalse(client1.session.get("authenticated")) # the user a client2 should still be authenticated self.assertTrue(client2.session.get("authenticated")) # the user should be deleted models.User.clean_deleted_sessions() # assert the user with expired sessions has being well deleted but the other remain self.assertEqual(len(models.User.objects.all()), 1) self.assertFalse(models.ServiceTicket.objects.all()) self.assertTrue(client2.session.get("authenticated")) @override_settings(CAS_AUTH_CLASS='cas_server.tests.auth.TestCachedAttributesAuthUser') def test_cached_attributs(self): """ Test gettting user attributes from cache for auth method that do not support direct fetch (link the ldap bind auth methode) """ client = get_auth_client() user = self.get_user(client) # if no cache is defined, the attributes are empty self.assertEqual(user.attributs, {}) user_attr = models.UserAttributes.objects.create(username=settings.CAS_TEST_USER) # if a cache is defined but without atrributes, also empty self.assertEqual(user.attributs, {}) user_attr.attributs = settings.CAS_TEST_ATTRIBUTES user_attr.save() # attributes are what is found in the cache self.assertEqual(user.attributs, settings.CAS_TEST_ATTRIBUTES) @override_settings(CAS_AUTH_CLASS='cas_server.auth.TestAuthUser') class TicketTestCase(TestCase, UserModels, BaseServicePattern): """tests for the tickets models""" def setUp(self): """Prepare the test context""" self.setup_service_patterns() self.service = 'http://127.0.0.1:45678' self.service_pattern = models.ServicePattern.objects.create( name="localhost", pattern=r"^https?://127\.0\.0\.1(:[0-9]+)?(/.*)?$", single_log_out=True ) models.ReplaceAttributName.objects.create(name="*", service_pattern=self.service_pattern) @staticmethod def get_ticket( user, ticket_class, service, service_pattern, renew=False, validate=False, validity_expired=False, timeout_expired=False, single_log_out=False, ): """Return a ticket""" ticket = user.get_ticket(ticket_class, service, service_pattern, renew) ticket.validate = validate ticket.single_log_out = single_log_out if validity_expired: ticket.creation = min( ticket.creation, (timezone.now() - timedelta(seconds=(ticket_class.VALIDITY + 10))) ) if timeout_expired: ticket.creation = min( ticket.creation, (timezone.now() - timedelta(seconds=(ticket_class.TIMEOUT + 10))) ) ticket.save() return ticket def test_clean_old_service_ticket(self): """test tickets clean_old_entries""" # ge an authenticated client client = get_auth_client() # get the user associated to the client user = self.get_user(client) # generate a ticket for that client, waiting for validation self.get_ticket(user, models.ServiceTicket, self.service, self.service_pattern) # generate another ticket for those validation time has expired self.get_ticket( user, models.ServiceTicket, self.service, self.service_pattern, validity_expired=True ) (httpd, host, port) = HttpParamsHandler.run()[0:3] service = "http://%s:%s" % (host, port) # generate a ticket with SLO having timeout reach self.get_ticket( user, models.ServiceTicket, service, self.service_pattern, timeout_expired=True, validate=True, single_log_out=True ) # there should be 3 tickets in the db self.assertEqual(len(models.ServiceTicket.objects.all()), 3) # we call the clean_old_entries method that should delete validated non SLO ticket and # expired non validated ticket and send SLO for SLO expired ticket before deleting then models.ServiceTicket.clean_old_entries() params = httpd.PARAMS # we successfully got a SLO request self.assertTrue(b'logoutRequest' in params and params[b'logoutRequest']) # only 1 ticket remain in the db self.assertEqual(len(models.ServiceTicket.objects.all()), 1) def test_json_attributes(self): """test the json storage of ``atrributs`` in ``_attributs``""" # ge an authenticated client client = get_auth_client() # get the user associated to the client user = self.get_user(client) ticket = models.ServiceTicket.objects.create( user=user, service=self.service, attributs=settings.CAS_TEST_ATTRIBUTES, service_pattern=self.service_pattern ) self.assertEqual(utils.json_encode(settings.CAS_TEST_ATTRIBUTES), ticket._attributs) ticket.delete() ticket = models.ServiceTicket.objects.create( user=user, service=self.service, service_pattern=self.service_pattern ) self.assertIsNone(ticket._attributs) self.assertIsNone(ticket.attributs) @mock.patch("cas_server.utils.last_version", lambda: "1.2.3") @override_settings(ADMINS=[("Ano Nymous", "ano.nymous@example.net")]) @override_settings(CAS_NEW_VERSION_EMAIL_WARNING=True) class NewVersionWarningTestCase(TestCase): """tests for the new version warning model""" @mock.patch("cas_server.models.VERSION", "0.1.2") def test_send_mails(self): """test the send_mails method with ADMINS and a new version available""" models.NewVersionWarning.send_mails() self.assertEqual(len(mail.outbox), 1) self.assertEqual( mail.outbox[0].subject, '%sA new version of django-cas-server is available' % settings.EMAIL_SUBJECT_PREFIX ) models.NewVersionWarning.send_mails() self.assertEqual(len(mail.outbox), 1) @mock.patch("cas_server.models.VERSION", "1.2.3") def test_send_mails_same_version(self): """test the send_mails method with with current version being the last""" models.NewVersionWarning.objects.create(version="0.1.2") models.NewVersionWarning.send_mails() self.assertEqual(len(mail.outbox), 0) @override_settings(ADMINS=[]) def test_send_mails_no_admins(self): """test the send_mails method without ADMINS""" models.NewVersionWarning.send_mails() self.assertEqual(len(mail.outbox), 0) @override_settings(CAS_NEW_VERSION_EMAIL_WARNING=False) def test_send_mails_disabled(self): """test the send_mails method if disabled""" models.NewVersionWarning.send_mails() self.assertEqual(len(mail.outbox), 0) django-cas-server-1.1.0/cas_server/tests/settings.py0000644000175000017500000000612513416345433024227 0ustar valentinvalentin00000000000000""" Django test settings for cas_server application. Generated by 'django-admin startproject' using Django 1.9.7. For more information on this file, see https://docs.djangoproject.com/en/1.9/topics/settings/ For the full list of settings and their values, see https://docs.djangoproject.com/en/1.9/ref/settings/ """ import os import django # Build paths inside the project like this: os.path.join(BASE_DIR, ...) BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Quick-start development settings - unsuitable for production # See https://docs.djangoproject.com/en/1.9/howto/deployment/checklist/ # SECURITY WARNING: keep the secret key used in production secret! SECRET_KEY = 'changeme' # SECURITY WARNING: don't run with debug turned on in production! DEBUG = True ALLOWED_HOSTS = [] # Application definition INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'cas_server', ] MIDDLEWARE = [ 'django.contrib.sessions.middleware.SessionMiddleware', 'django.middleware.common.CommonMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.locale.LocaleMiddleware', ] if django.VERSION < (1, 10): MIDDLEWARE_CLASSES = MIDDLEWARE TEMPLATES = [ { 'APP_DIRS': True, 'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [], 'OPTIONS': { 'context_processors': [ 'django.template.context_processors.debug', 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages' ] } } ] ROOT_URLCONF = 'cas_server.tests.urls' # Database # https://docs.djangoproject.com/en/1.9/ref/settings/#databases DATABASES = { 'default': { 'ENGINE': 'django.db.backends.sqlite3', 'NAME': ':memory:', } } # Internationalization # https://docs.djangoproject.com/en/1.9/topics/i18n/ LANGUAGE_CODE = 'en-us' TIME_ZONE = 'UTC' USE_I18N = True USE_L10N = True USE_TZ = True # Static files (CSS, JavaScript, Images) # https://docs.djangoproject.com/en/1.9/howto/static-files/ STATIC_URL = '/static/' CAS_NEW_VERSION_HTML_WARNING = False CAS_NEW_VERSION_EMAIL_WARNING = False LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'cas_file': { 'format': '%(asctime)s %(levelname)s %(message)s' }, }, 'handlers': { 'cas_stream': { 'level': 'INFO', 'class': 'logging.StreamHandler', 'formatter': 'cas_file', }, }, 'loggers': { 'cas_server': { 'handlers': ['cas_stream'], 'level': 'INFO', 'propagate': True, }, }, } django-cas-server-1.1.0/cas_server/tests/test_utils.py0000644000175000017500000002650213416345433024567 0ustar valentinvalentin00000000000000# -*- coding: utf-8 -*- # 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 General Public License version 3 for # more details. # # You should have received a copy of the GNU General Public License version 3 # along with this program; if not, write to the Free Software Foundation, Inc., 51 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # (c) 2016 Valentin Samir """Tests module for utils""" from django.test import TestCase, RequestFactory from django.db import connection import six import warnings import datetime from cas_server import utils class CheckPasswordCase(TestCase): """Tests for the utils function `utils.check_password`""" def setUp(self): """Generate random bytes string that will be used ass passwords""" self.password1 = utils.gen_saml_id() self.password2 = utils.gen_saml_id() if not isinstance(self.password1, bytes): # pragma: no cover executed only in python3 self.password1 = self.password1.encode("utf8") self.password2 = self.password2.encode("utf8") def test_setup(self): """check that generated password are bytes""" self.assertIsInstance(self.password1, bytes) self.assertIsInstance(self.password2, bytes) def test_plain(self): """test the plain auth method""" self.assertTrue(utils.check_password("plain", self.password1, self.password1, "utf8")) self.assertFalse(utils.check_password("plain", self.password1, self.password2, "utf8")) def test_plain_unicode(self): """test the plain auth method with unicode input""" self.assertTrue( utils.check_password( "plain", self.password1.decode("utf8"), self.password1.decode("utf8"), "utf8" ) ) self.assertFalse( utils.check_password( "plain", self.password1.decode("utf8"), self.password2.decode("utf8"), "utf8" ) ) def test_crypt(self): """test the crypt auth method""" salts = ["$6$UVVAQvrMyXMF3FF3", "aa"] hashed_password1 = [] for salt in salts: if six.PY3: hashed_password1.append( utils.crypt.crypt( self.password1.decode("utf8"), salt ).encode("utf8") ) else: hashed_password1.append(utils.crypt.crypt(self.password1, salt)) for hp1 in hashed_password1: self.assertTrue(utils.check_password("crypt", self.password1, hp1, "utf8")) self.assertFalse(utils.check_password("crypt", self.password2, hp1, "utf8")) with self.assertRaises(ValueError): utils.check_password("crypt", self.password1, b"$truc$s$dsdsd", "utf8") def test_ldap_password_valid(self): """test the ldap auth method with all the schemes""" salt = b"UVVAQvrMyXMF3FF3" schemes_salt = [b"{SMD5}", b"{SSHA}", b"{SSHA256}", b"{SSHA384}", b"{SSHA512}"] schemes_nosalt = [b"{MD5}", b"{SHA}", b"{SHA256}", b"{SHA384}", b"{SHA512}"] hashed_password1 = [] for scheme in schemes_salt: hashed_password1.append( utils.LdapHashUserPassword.hash(scheme, self.password1, salt, charset="utf8") ) for scheme in schemes_nosalt: hashed_password1.append( utils.LdapHashUserPassword.hash(scheme, self.password1, charset="utf8") ) hashed_password1.append( utils.LdapHashUserPassword.hash( b"{CRYPT}", self.password1, b"$6$UVVAQvrMyXMF3FF3", charset="utf8" ) ) for hp1 in hashed_password1: self.assertIsInstance(hp1, bytes) self.assertTrue(utils.check_password("ldap", self.password1, hp1, "utf8")) self.assertFalse(utils.check_password("ldap", self.password2, hp1, "utf8")) def test_ldap_password_fail(self): """test the ldap auth method with malformed hash or bad schemes""" salt = b"UVVAQvrMyXMF3FF3" schemes_salt = [b"{SMD5}", b"{SSHA}", b"{SSHA256}", b"{SSHA384}", b"{SSHA512}"] schemes_nosalt = [b"{MD5}", b"{SHA}", b"{SHA256}", b"{SHA384}", b"{SHA512}"] # first try to hash with bad parameters with self.assertRaises(utils.LdapHashUserPassword.BadScheme): utils.LdapHashUserPassword.hash(b"TOTO", self.password1) for scheme in schemes_nosalt: with self.assertRaises(utils.LdapHashUserPassword.BadScheme): utils.LdapHashUserPassword.hash(scheme, self.password1, salt) for scheme in schemes_salt: with self.assertRaises(utils.LdapHashUserPassword.BadScheme): utils.LdapHashUserPassword.hash(scheme, self.password1) with self.assertRaises(utils.LdapHashUserPassword.BadSalt): utils.LdapHashUserPassword.hash(b'{CRYPT}', self.password1, b"$truc$toto") # then try to check hash with bad hashes with self.assertRaises(utils.LdapHashUserPassword.BadHash): utils.check_password("ldap", self.password1, b"TOTOssdsdsd", "utf8") for scheme in schemes_salt: # bad length with self.assertRaises(utils.LdapHashUserPassword.BadHash): utils.check_password("ldap", self.password1, scheme + b"dG90b3E8ZHNkcw==", "utf8") # bad base64 with self.assertRaises(utils.LdapHashUserPassword.BadHash): utils.check_password("ldap", self.password1, scheme + b"dG90b3E8ZHNkcw", "utf8") def test_hex(self): """test all the hex_HASH method: the hashed password is a simple hash of the password""" hashes = ["md5", "sha1", "sha224", "sha256", "sha384", "sha512"] hashed_password1 = [] for hash_scheme in hashes: hashed_password1.append( ( "hex_%s" % hash_scheme, getattr(utils.hashlib, hash_scheme)(self.password1).hexdigest() ) ) for (method, hp1) in hashed_password1: self.assertTrue(utils.check_password(method, self.password1, hp1, "utf8")) self.assertFalse(utils.check_password(method, self.password2, hp1, "utf8")) def test_bad_method(self): """try to check password with a bad method, should raise a ValueError""" with self.assertRaises(ValueError): utils.check_password("test", self.password1, b"$truc$s$dsdsd", "utf8") class UtilsTestCase(TestCase): """tests for some little utils functions""" def test_import_attr(self): """ test the import_attr function. Feeded with a dotted path string, it should import the dotted module and return that last componend of the dotted path (function, class or variable) """ with self.assertRaises(ImportError): utils.import_attr('toto.titi.tutu') with self.assertRaises(AttributeError): utils.import_attr('cas_server.utils.toto') with self.assertRaises(ValueError): utils.import_attr('toto') self.assertEqual( utils.import_attr('cas_server.default_app_config'), 'cas_server.apps.CasAppConfig' ) self.assertEqual(utils.import_attr(utils), utils) def test_update_url(self): """ test the update_url function. Given an url with possible GET parameter and a dict the function build a url with GET parameters updated by the dictionnary """ url1 = utils.update_url(u"https://www.example.com?toto=1", {u"tata": u"2"}) url2 = utils.update_url(b"https://www.example.com?toto=1", {b"tata": b"2"}) self.assertEqual(url1, u"https://www.example.com?tata=2&toto=1") self.assertEqual(url2, u"https://www.example.com?tata=2&toto=1") url3 = utils.update_url(u"https://www.example.com?toto=1", {u"toto": u"2"}) self.assertEqual(url3, u"https://www.example.com?toto=2") def test_crypt_salt_is_valid(self): """test the function crypt_salt_is_valid who test if a crypt salt is valid""" self.assertFalse(utils.crypt_salt_is_valid("")) # len 0 self.assertFalse(utils.crypt_salt_is_valid("a")) # len 1 self.assertFalse(utils.crypt_salt_is_valid("$$")) # start with $ followed by $ self.assertFalse(utils.crypt_salt_is_valid("$toto")) # start with $ but no secondary $ self.assertFalse(utils.crypt_salt_is_valid("$toto$toto")) # algorithm toto not known def test_get_current_url(self): """test the function get_current_url""" factory = RequestFactory() request = factory.get('/truc/muche?test=1') self.assertEqual(utils.get_current_url(request), 'http://testserver/truc/muche?test=1') self.assertEqual( utils.get_current_url(request, ignore_params={'test'}), 'http://testserver/truc/muche' ) def test_get_tuple(self): """test the function get_tuple""" test_tuple = (1, 2, 3) for index, value in enumerate(test_tuple): self.assertEqual(utils.get_tuple(test_tuple, index), value) self.assertEqual(utils.get_tuple(test_tuple, 3), None) self.assertEqual(utils.get_tuple(test_tuple, 3, 'toto'), 'toto') self.assertEqual(utils.get_tuple(None, 3), None) def test_last_version(self): """ test the function last_version. An internet connection is needed, if you do not have one, this test will fail and you should ignore it. """ try: # first check if pypi is available utils.requests.get("https://pypi.org/simple/django-cas-server/") except utils.requests.exceptions.RequestException: warnings.warn( ( "Pypi seems not available, perhaps you do not have internet access. " "Consequently, the test cas_server.tests.test_utils.UtilsTestCase.test_last_" "version is ignored" ), RuntimeWarning ) else: version = utils.last_version() self.assertIsInstance(version, six.text_type) self.assertEqual(len(version.split('.')), 3) # version is cached 24h so calling it a second time should return the save value self.assertEqual(version, utils.last_version()) def test_dictfetchall(self): """test the function dictfetchall""" with connection.cursor() as curs: curs.execute("SELECT * FROM django_migrations") results = utils.dictfetchall(curs) self.assertIsInstance(results, list) self.assertTrue(len(results) > 0) for result in results: self.assertIsInstance(result, dict) self.assertIn('applied', result) self.assertIsInstance(result['applied'], datetime.datetime) def test_regexpr_validator(self): """test the function regexpr_validator""" utils.regexpr_validator("^a$") with self.assertRaises(utils.ValidationError): utils.regexpr_validator("[") django-cas-server-1.1.0/cas_server/tests/test_view.py0000644000175000017500000026121413416345433024402 0ustar valentinvalentin00000000000000# -*- coding: utf-8 -*- # 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 General Public License version 3 for # more details. # # You should have received a copy of the GNU General Public License version 3 # along with this program; if not, write to the Free Software Foundation, Inc., 51 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # (c) 2016 Valentin Samir """Tests module for views""" from cas_server.default_settings import settings import django from django.test import TestCase, Client from django.test.utils import override_settings from django.utils import timezone import random import json import mock from lxml import etree from six.moves import range from cas_server import models from cas_server import utils from cas_server.tests.utils import ( copy_form, get_login_page_params, get_auth_client, get_user_ticket_request, get_pgt, get_proxy_ticket, get_validated_ticket, HttpParamsHandler, Http404Handler ) from cas_server.tests.mixin import BaseServicePattern, XmlContent, CanLogin @override_settings(CAS_AUTH_CLASS='cas_server.auth.TestAuthUser') class LoginTestCase(TestCase, BaseServicePattern, CanLogin): """Tests for the login view""" def setUp(self): """Prepare the test context:""" # we prepare a bunch a service url and service patterns for tests self.setup_service_patterns() @override_settings(CAS_NEW_VERSION_HTML_WARNING=True) @mock.patch("cas_server.utils.last_version", lambda: "1.2.3") @mock.patch("cas_server.utils.VERSION", "0.1.2") def test_new_version_available_ok(self): """test the new version info box""" client = Client() response = client.get("/login") self.assertIn(b"A new version of the application is available", response.content) @override_settings(CAS_NEW_VERSION_HTML_WARNING=True) @mock.patch("cas_server.utils.last_version", lambda: None) @mock.patch("cas_server.utils.VERSION", "0.1.2") def test_new_version_available_badpypi(self): """ test the new version info box if pypi is not available (unable to retreive last version) """ client = Client() response = client.get("/login") self.assertNotIn(b"A new version of the application is available", response.content) @override_settings(CAS_NEW_VERSION_HTML_WARNING=False) def test_new_version_available_disabled(self): """test the new version info box is disabled""" client = Client() response = client.get("/login") self.assertNotIn(b"A new version of the application is available", response.content) @override_settings(CAS_INFO_MESSAGES_ORDER=["cas_explained"]) def test_messages_info_box_enabled(self): """test that the message info-box is displayed then enabled""" client = Client() response = client.get("/login") self.assertIn( b"The Central Authentication Service grants you access to most of our websites by ", response.content ) @override_settings(CAS_INFO_MESSAGES_ORDER=[]) def test_messages_info_box_disabled(self): """test that the message info-box is not displayed then disabled""" client = Client() response = client.get("/login") self.assertNotIn( b"The Central Authentication Service grants you access to most of our websites by ", response.content ) # test1 and test2 are malformed and should be ignored, test3 is ok, test5 do not # exists and should be ignored @override_settings(CAS_INFO_MESSAGES_ORDER=["test1", "test2", "test3", "test5"]) @override_settings(CAS_INFO_MESSAGES={ "test1": "test", # not a dict, should be ignored "test2": {"type": "success"}, # not "message" key, should be ignored "test3": {"message": "test3"}, "test4": {"message": "test4"}, }) def test_messages_info_box_bad_messages(self): """test that mal formated messages dict are ignored""" client = Client() # not errors should be raises response = client.get("/login") # test3 is ok est should be there self.assertIn(b"test3", response.content) # test4 is not in CAS_INFO_MESSAGES_ORDER and should not be there self.assertNotIn(b"test4", response.content) def test_login_view_post_goodpass_goodlt(self): """Test a successul login""" # we get a client who fetch a frist time the login page and the login form default # parameters client, params = get_login_page_params() # we set username/password in the form params["username"] = settings.CAS_TEST_USER params["password"] = settings.CAS_TEST_PASSWORD # the LoginTicket in the form should match a valid LT in the user session self.assertTrue(params['lt'] in client.session['lt']) # we post a login attempt response = client.post('/login', params) # as username/password/lt are all valid, the login should succed self.assert_logged(client, response) # The LoginTicket is conssumed and should no longer be valid self.assertTrue(params['lt'] not in client.session['lt']) def test_login_post_missing_params(self): """Test a login attempt with missing POST parameters (username or password or both)""" # we get a client who fetch a frist time the login page and the login form default # parameters client, params = get_login_page_params() # we set only set username params["username"] = settings.CAS_TEST_USER # we post a login attempt response = client.post('/login', params) # as the LT is not valid, login should fail self.assert_login_failed(client, response) # we get a client who fetch a frist time the login page and the login form default # parameters client, params = get_login_page_params() # we set only set password params["password"] = settings.CAS_TEST_PASSWORD # we post a login attempt response = client.post('/login', params) # as the LT is not valid, login should fail self.assert_login_failed(client, response) # we get a client who fetch a frist time the login page and the login form default # parameters client, params = get_login_page_params() # we set neither username nor password # we post a login attempt response = client.post('/login', params) # as the LT is not valid, login should fail self.assert_login_failed(client, response) def test_login_view_post_goodpass_goodlt_warn(self): """Test a successul login requesting to be warned before creating services tickets""" # get a client and initial login params client, params = get_login_page_params() # set valids usernames/passswords params["username"] = settings.CAS_TEST_USER params["password"] = settings.CAS_TEST_PASSWORD # this time, we check the warn checkbox params["warn"] = "on" # postings login request response = client.post('/login', params) # as username/password/lt are all valid, the login should succed and warn be enabled self.assert_logged(client, response, warn=True) def test_lt_max(self): """Check we only keep the last 100 Login Ticket for a user""" # get a client and initial login params client, params = get_login_page_params() # get a first LT that should be valid current_lt = params["lt"] # we keep the last 100 generated LT by user, so after having generated `i_in_test` we # test if `current_lt` is still valid i_in_test = random.randint(0, 99) # after `i_not_in_test` `current_lt` should be valid not more i_not_in_test = random.randint(101, 150) # start generating 150 LT for i in range(150): if i == i_in_test: # before more than 100 LT generated, the first TL should be valid self.assertTrue(current_lt in client.session['lt']) if i == i_not_in_test: # after more than 100 LT generated, the first LT should be valid no more self.assertTrue(current_lt not in client.session['lt']) # assert that we do not keep more that 100 valid LT self.assertTrue(len(client.session['lt']) <= 100) # generate a new LT by getting the login page client, params = get_login_page_params(client) # in the end, we still have less that 100 valid LT self.assertTrue(len(client.session['lt']) <= 100) def test_login_view_post_badlt(self): """Login attempt with a bad LoginTicket, login should fail""" # get a client and initial login params client, params = get_login_page_params() # set valid username/password params["username"] = settings.CAS_TEST_USER params["password"] = settings.CAS_TEST_PASSWORD # set a bad LT params["lt"] = 'LT-random' # posting the login request response = client.post('/login', params) # as the LT is not valid, login should fail self.assert_login_failed(client, response) # the reason why login has failed is displayed to the user self.assertTrue(b"Invalid login ticket" in response.content) def test_login_view_post_badpass_good_lt(self): """Login attempt with a bad password""" # get a client and initial login params client, params = get_login_page_params() # set valid username but invalid password params["username"] = settings.CAS_TEST_USER params["password"] = "test2" # posting the login request response = client.post('/login', params) # as the password is wrong, login should fail self.assert_login_failed(client, response) # the reason why login has failed is displayed to the user self.assertTrue( ( b"The credentials you provided cannot be " b"determined to be authentic" ) in response.content ) def assert_ticket_attributes(self, client, ticket_value): """check the ticket attributes in the db""" # Get get current session user in the db user = models.User.objects.get( username=settings.CAS_TEST_USER, session_key=client.session.session_key ) # we should find exactly one user self.assertTrue(user) # get the ticker object corresponting to `ticket_value` ticket = models.ServiceTicket.objects.get(value=ticket_value) # chek that the ticket is well attributed to the user self.assertEqual(ticket.user, user) # check that the user attributes match the attributes registered on the ticket self.assertEqual(ticket.attributs, settings.CAS_TEST_ATTRIBUTES) # check that the ticket has not being validated yet self.assertEqual(ticket.validate, False) # check that the service pattern registered on the ticket is the on we use for tests self.assertEqual(ticket.service_pattern, self.service_pattern) def assert_service_ticket(self, client, response): """check that a ticket is well emited when requested on a allowed service""" # On ticket emission, we should be redirected to the service url, setting the ticket # GET parameter self.assertEqual(response.status_code, 302) self.assertTrue(response.has_header('Location')) self.assertTrue( response['Location'].startswith( "https://www.example.com?ticket=%s-" % settings.CAS_SERVICE_TICKET_PREFIX ) ) # check that the value of the ticket GET parameter match the value of the ticket # created in the db ticket_value = response['Location'].split('ticket=')[-1] self.assert_ticket_attributes(client, ticket_value) def test_view_login_get_allowed_service(self): """Request a ticket for an allowed service by an unauthenticated client""" # get a bare new http client client = Client() # we are not authenticated and are asking for a ticket for https://www.example.com # which is a valid service matched by self.service_pattern response = client.get("/login?service=https://www.example.com") # the login page should be displayed self.assertEqual(response.status_code, 200) # we warn the user why it need to authenticated self.assertTrue( ( b"Authentication required by service " b"example (https://www.example.com)" ) in response.content ) @override_settings(CAS_SHOW_SERVICE_MESSAGES=False) def test_view_login_get_allowed_service_no_message(self): """Request a ticket for an allowed service by an unauthenticated client""" # get a bare new http client client = Client() # we are not authenticated and are asking for a ticket for https://www.example.com # which is a valid service matched by self.service_pattern response = client.get("/login?service=https://www.example.com") # the login page should be displayed self.assertEqual(response.status_code, 200) # we warn the user why it need to authenticated self.assertFalse( ( b"Authentication required by service " b"example (https://www.example.com)" ) in response.content ) def test_view_login_get_denied_service(self): """Request a ticket for an denied service by an unauthenticated client""" # get a bare new http client client = Client() # we are not authenticated and are asking for a ticket for https://www.example.net # which is NOT a valid service response = client.get("/login?service=https://www.example.net") self.assertEqual(response.status_code, 200) # we warn the user that https://www.example.net is not an allowed service url self.assertTrue(b"Service https://www.example.net not allowed" in response.content) @override_settings(CAS_SHOW_SERVICE_MESSAGES=False) def test_view_login_get_denied_service_no_message(self): """Request a ticket for an denied service by an unauthenticated client""" # get a bare new http client client = Client() # we are not authenticated and are asking for a ticket for https://www.example.net # which is NOT a valid service response = client.get("/login?service=https://www.example.net") self.assertEqual(response.status_code, 200) # we warn the user that https://www.example.net is not an allowed service url self.assertFalse(b"Service https://www.example.net not allowed" in response.content) def test_view_login_get_auth_allowed_service(self): """Request a ticket for an allowed service by an authenticated client""" # get a client that is already authenticated client = get_auth_client() # ask for a ticket for https://www.example.com response = client.get("/login?service=https://www.example.com") # as https://www.example.com is a valid service a ticket should be created and the # user redirected to the service url self.assert_service_ticket(client, response) def test_view_login_get_auth_allowed_service_warn(self): """Request a ticket for an allowed service by an authenticated client""" # get a client that is already authenticated and has ask to be warned befor we # generated a ticket client = get_auth_client(warn="on") # ask for a ticket for https://www.example.com response = client.get("/login?service=https://www.example.com") # we display a warning to the user, asking him to validate the ticket creation (insted # a generating and redirecting directly to the service url) self.assertEqual(response.status_code, 200) self.assertTrue( ( b"Authentication has been required by service " b"example (https://www.example.com)" ) in response.content ) # get the displayed form parameters params = copy_form(response.context["form"]) # we post, confirming we want a ticket response = client.post("/login", params) # as https://www.example.com is a valid service a ticket should be created and the # user redirected to the service url self.assert_service_ticket(client, response) def test_view_login_get_auth_denied_service(self): """Request a ticket for a not allowed service by an authenticated client""" # get a client that is already authenticated client = get_auth_client() # we are authenticated and are asking for a ticket for https://www.example.org # which is NOT a valid service response = client.get("/login?service=https://www.example.org") self.assertEqual(response.status_code, 200) # we warn the user that https://www.example.net is not an allowed service url # NO ticket are created self.assertTrue(b"Service https://www.example.org not allowed" in response.content) def test_user_logged_not_in_db(self): """If the user is logged but has been delete from the database, it should be logged out""" # get a client that is already authenticated client = get_auth_client() # delete the user in the db models.User.objects.get( username=settings.CAS_TEST_USER, session_key=client.session.session_key ).delete() # fetch the login page response = client.get("/login") # The user should be logged out self.assert_login_failed(client, response, code=302) # and redirected to the login page. We branch depending on the version a django as # the test client behaviour changed after django 1.9 if django.VERSION < (1, 9): # pragma: no cover coverage is computed with dango 1.9 self.assertEqual(response["Location"], "http://testserver/login") else: self.assertEqual(response["Location"], "/login?") def test_service_restrict_user(self): """Testing the restric user capability from a service""" # get a client that is already authenticated client = get_auth_client() # trying to get a ticket from a service url matched by a service pattern having a # restriction on the usernames allowed to get tickets. the test user username is not one # of this username. response = client.get("/login", {'service': self.service_restrict_user_fail}) self.assertEqual(response.status_code, 200) # the ticket is not created and a warning is displayed to the user self.assertTrue(b"Username not allowed" in response.content) # same but with the tes user username being one of the allowed usernames response = client.get("/login", {'service': self.service_restrict_user_success}) # the ticket is created and we are redirected to the service url self.assertEqual(response.status_code, 302) self.assertTrue( response["Location"].startswith("%s?ticket=" % self.service_restrict_user_success) ) def test_service_filter(self): """Test the filtering on user attributes""" # get a client that is already authenticated client = get_auth_client() # trying to get a ticket from a service url matched by a service pattern having # a restriction on the user attributes. The test user if ailing these restrictions # We try first with a single value attribut (aka a string) and then with # a multi values attributs (aka a list of strings) for service in [self.service_filter_fail, self.service_filter_fail_alt]: response = client.get("/login", {'service': service}) # the ticket is not created and a warning is displayed to the user self.assertEqual(response.status_code, 200) self.assertTrue(b"User characteristics not allowed" in response.content) # same but with rectriction that a valid upon the test user attributes response = client.get("/login", {'service': self.service_filter_success}) # the ticket us created and the user redirected to the service url self.assertEqual(response.status_code, 302) self.assertTrue(response["Location"].startswith("%s?ticket=" % self.service_filter_success)) def test_service_user_field(self): """Test using a user attribute as username: case on if the attribute exists or not""" # get a client that is already authenticated client = get_auth_client() # trying to get a ticket from a service url matched by a service pattern that use # a particular attribute has username. The test user do NOT have this attribute response = client.get("/login", {'service': self.service_field_needed_fail}) # the ticket is not created and a warning is displayed to the user self.assertEqual(response.status_code, 200) self.assertTrue(b"The attribute uid is needed to use that service" in response.content) # same but with a attribute that the test user has response = client.get("/login", {'service': self.service_field_needed_success}) # the ticket us created and the user redirected to the service url self.assertEqual(response.status_code, 302) self.assertTrue( response["Location"].startswith("%s?ticket=" % self.service_field_needed_success) ) @override_settings(CAS_TEST_ATTRIBUTES={'alias': []}) def test_service_user_field_evaluate_to_false(self): """ Test using a user attribute as username: case the attribute exists but evaluate to False """ # get a client that is already authenticated client = get_auth_client() # trying to get a ticket from a service url matched by a service pattern that use # a particular attribute has username. The test user have this attribute, but it is # evaluated to False (eg an empty string "" or an empty list []) response = client.get("/login", {"service": self.service_field_needed_success}) # the ticket is not created and a warning is displayed to the user self.assertEqual(response.status_code, 200) self.assertTrue(b"The attribute alias is needed to use that service" in response.content) def test_gateway(self): """test gateway parameter""" # First with an authenticated client that fail to get a ticket for a service service = "https://restrict_user_fail.example.com" # get a client that is already authenticated client = get_auth_client() # the authenticated client fail to get a ticket for some reason response = client.get("/login", {'service': service, 'gateway': 'on'}) # as gateway is set, he is redirected to the service url without any ticket self.assertEqual(response.status_code, 302) self.assertEqual(response["Location"], service) # second for an user not yet authenticated on a valid service client = Client() # the client fail to get a ticket since he is not yep authenticated response = client.get('/login', {'service': service, 'gateway': 'on'}) # as gateway is set, he is redirected to the service url without any ticket self.assertEqual(response.status_code, 302) self.assertEqual(response["Location"], service) def test_renew(self): """test the authentication renewal request from a service""" # use the default test service service = "https://www.example.com" # get a client that is already authenticated client = get_auth_client() # ask for a ticket for the service but aks for authentication renewal response = client.get("/login", {'service': service, 'renew': 'on'}) # we are ask to reauthenticate and tell the user why self.assertEqual(response.status_code, 200) self.assertTrue( ( b"Authentication renewal required by " b"service example (https://www.example.com)" ) in response.content ) # get the form default parameter params = copy_form(response.context["form"]) # set valid username/password params["username"] = settings.CAS_TEST_USER params["password"] = settings.CAS_TEST_PASSWORD # the renew parameter from the form should be True self.assertEqual(params["renew"], True) # post the authentication request response = client.post("/login", params) # the request succed, a ticket is created and we are redirected to the service url self.assertEqual(response.status_code, 302) ticket_value = response['Location'].split('ticket=')[-1] ticket = models.ServiceTicket.objects.get(value=ticket_value) # the created ticket is marked has being gottent after a renew. Futher testing about # renewing authentication is done in the validate and serviceValidate views tests self.assertEqual(ticket.renew, True) @override_settings(CAS_SHOW_SERVICE_MESSAGES=False) def test_renew_message_disabled(self): """test the authentication renewal request from a service""" # use the default test service service = "https://www.example.com" # get a client that is already authenticated client = get_auth_client() # ask for a ticket for the service but aks for authentication renewal response = client.get("/login", {'service': service, 'renew': 'on'}) # we are ask to reauthenticate and tell the user why self.assertEqual(response.status_code, 200) self.assertFalse( ( b"Authentication renewal required by " b"service example (https://www.example.com)" ) in response.content ) # get the form default parameter params = copy_form(response.context["form"]) # set valid username/password params["username"] = settings.CAS_TEST_USER params["password"] = settings.CAS_TEST_PASSWORD # the renew parameter from the form should be True self.assertEqual(params["renew"], True) # post the authentication request response = client.post("/login", params) # the request succed, a ticket is created and we are redirected to the service url self.assertEqual(response.status_code, 302) ticket_value = response['Location'].split('ticket=')[-1] ticket = models.ServiceTicket.objects.get(value=ticket_value) # the created ticket is marked has being gottent after a renew. Futher testing about # renewing authentication is done in the validate and serviceValidate views tests self.assertEqual(ticket.renew, True) @override_settings(CAS_ENABLE_AJAX_AUTH=True) def test_ajax_login_required(self): """ test ajax, login required. The ajax methods allow the log a user in using javascript. For doing so, every 302 redirection a replaced by a 200 returning a json with the url to redirect to. By default, ajax login is disabled. If CAS_ENABLE_AJAX_AUTH is True, ajax login is enable and only page on the same domain as the CAS can do ajax request. To allow pages on other domains, you need to use CORS. You can use the django app corsheaders for that. Be carefull to only allow domains you completly trust as any javascript on these domaine will be able to authenticate as the user. """ # get a bare client client = Client() # fetch the login page setting up the custom header HTTP_X_AJAX to tell we wish to de # ajax requests response = client.get("/login", HTTP_X_AJAX='on') # we get a json as response telling us the user need to be authenticated self.assertEqual(response.status_code, 200) data = json.loads(response.content.decode("utf8")) self.assertEqual(data["status"], "error") self.assertEqual(data["detail"], "login required") self.assertEqual(data["url"], "/login") @override_settings(CAS_ENABLE_AJAX_AUTH=True) def test_ajax_logged_user_deleted(self): """test ajax user logged deleted: login required""" # get a client that is already authenticated client = get_auth_client() # delete the user in the db user = models.User.objects.get( username=settings.CAS_TEST_USER, session_key=client.session.session_key ) user.delete() # fetch the login page with ajax on response = client.get("/login", HTTP_X_AJAX='on') # we get a json telling us that the user need to authenticate self.assertEqual(response.status_code, 200) data = json.loads(response.content.decode("utf8")) self.assertEqual(data["status"], "error") self.assertEqual(data["detail"], "login required") self.assertEqual(data["url"], "/login") @override_settings(CAS_ENABLE_AJAX_AUTH=True) def test_ajax_logged(self): """test ajax user is successfully logged""" # get a client that is already authenticated client = get_auth_client() # fetch the login page with ajax on response = client.get("/login", HTTP_X_AJAX='on') # we get a json telling us that the user is well authenticated self.assertEqual(response.status_code, 200) data = json.loads(response.content.decode("utf8")) self.assertEqual(data["status"], "success") self.assertEqual(data["detail"], "logged") @override_settings(CAS_ENABLE_AJAX_AUTH=True) def test_ajax_get_ticket_success(self): """test ajax retrieve a ticket for an allowed service""" # using the default test service service = "https://www.example.com" # get a client that is already authenticated client = get_auth_client() # fetch the login page with ajax on response = client.get("/login", {'service': service}, HTTP_X_AJAX='on') # we get a json telling us that the ticket has being created # and we get the url to fetch to authenticate the user to the service # contening the ticket has GET parameter self.assertEqual(response.status_code, 200) data = json.loads(response.content.decode("utf8")) self.assertEqual(data["status"], "success") self.assertEqual(data["detail"], "auth") self.assertTrue(data["url"].startswith('%s?ticket=' % service)) def test_ajax_get_ticket_success_alt(self): """ test ajax retrieve a ticket for an allowed service. Same as above but with CAS_ENABLE_AJAX_AUTH=False """ # using the default test service service = "https://www.example.com" # get a client that is already authenticated client = get_auth_client() # fetch the login page with ajax on response = client.get("/login", {'service': service}, HTTP_X_AJAX='on') # as CAS_ENABLE_AJAX_AUTH is False the ajax request is ignored and word normally: # 302 redirect to the service url with ticket as GET parameter. javascript # cannot retieve the ticket info and try follow the redirect to an other domain and fail # silently self.assertEqual(response.status_code, 302) @override_settings(CAS_ENABLE_AJAX_AUTH=True) def test_ajax_get_ticket_fail(self): """test ajax retrieve a ticket for a denied service""" # using a denied service url service = "https://www.example.org" # get a client that is already authenticated client = get_auth_client() # fetch the login page with ajax on response = client.get("/login", {'service': service}, HTTP_X_AJAX='on') # we get a json telling us that the service is not allowed self.assertEqual(response.status_code, 200) data = json.loads(response.content.decode("utf8")) self.assertEqual(data["status"], "error") self.assertEqual(data["detail"], "auth") self.assertEqual(data["messages"][0]["level"], "error") self.assertEqual( data["messages"][0]["message"], "Service https://www.example.org not allowed." ) @override_settings(CAS_ENABLE_AJAX_AUTH=True) def test_ajax_get_ticket_warn(self): """test get a ticket but user asked to be warned""" # using the default test service service = "https://www.example.com" # get a client that is already authenticated wth warn on client = get_auth_client(warn="on") # fetch the login page with ajax on response = client.get("/login", {'service': service}, HTTP_X_AJAX='on') # we get a json telling us that we cannot get a ticket transparently and that the # user has asked to be warned self.assertEqual(response.status_code, 200) data = json.loads(response.content.decode("utf8")) self.assertEqual(data["status"], "error") self.assertEqual(data["detail"], "confirmation needed") @override_settings(CAS_AUTH_CLASS='cas_server.auth.TestAuthUser') class LogoutTestCase(TestCase): """test fot the logout view""" def setUp(self): """Prepare the test context""" # for testing SingleLogOut we need to use a service on localhost were we lanch # a simple one request http server self.service = 'http://127.0.0.1:45678' self.service_pattern = models.ServicePattern.objects.create( name="localhost", pattern=r"^https?://127\.0\.0\.1(:[0-9]+)?(/.*)?$", single_log_out=True ) # return all user attributes models.ReplaceAttributName.objects.create(name="*", service_pattern=self.service_pattern) def test_logout(self): """logout is idempotent""" # get a bare client client = Client() # call logout client.get("/logout") # we are still not logged self.assertFalse(client.session.get("username")) self.assertFalse(client.session.get("authenticated")) def test_logout_view(self): """test simple logout, logout only an user from one and only one sessions""" # get two authenticated client with the same test user (but two different sessions) client = get_auth_client() client2 = get_auth_client() # fetch login, the first client is well authenticated response = client.get("/login") self.assertEqual(response.status_code, 200) self.assertTrue( ( b"You have successfully logged into " b"the Central Authentication Service" ) in response.content ) # and session variable are well self.assertTrue(client.session["username"] == settings.CAS_TEST_USER) self.assertTrue(client.session["authenticated"] is True) # call logout with the first client response = client.get("/logout") # the client is logged out self.assertEqual(response.status_code, 200) self.assertTrue( ( b"You have successfully logged out from " b"the Central Authentication Service" ) in response.content ) # and session variable a well cleaned self.assertFalse(client.session.get("username")) self.assertFalse(client.session.get("authenticated")) # client2 is still logged self.assertTrue(client2.session["username"] == settings.CAS_TEST_USER) self.assertTrue(client2.session["authenticated"] is True) response = client.get("/login") # fetch login, the second client is well authenticated self.assertEqual(response.status_code, 200) self.assertFalse( ( b"You have successfully logged into " b"the Central Authentication Service" ) in response.content ) def test_logout_from_all_session(self): """test logout from all my session""" # get two authenticated client with the same test user (but two different sessions) client = get_auth_client() client2 = get_auth_client() # call logout with the first client and ask to be logged out from all of this user sessions client.get("/logout?all=1") # both client are logged out self.assertFalse(client.session.get("username")) self.assertFalse(client.session.get("authenticated")) self.assertFalse(client2.session.get("username")) self.assertFalse(client2.session.get("authenticated")) def assert_redirect_to_service(self, client, response): """assert logout redirect to parameter""" # assert a redirection with a service self.assertEqual(response.status_code, 302) self.assertTrue(response.has_header("Location")) self.assertEqual(response["Location"], "https://www.example.com") response = client.get("/login") self.assertEqual(response.status_code, 200) # assert we are not longer logged in self.assertFalse( ( b"You have successfully logged into " b"the Central Authentication Service" ) in response.content ) def test_logout_view_url(self): """test logout redirect to url parameter""" # get a client that is authenticated client = get_auth_client() # logout with an url paramer response = client.get('/logout?url=https://www.example.com') # we are redirected to the addresse of the url parameter self.assert_redirect_to_service(client, response) def test_logout_view_service(self): """test logout redirect to service parameter""" # get a client that is authenticated client = get_auth_client() # logout with a service parameter response = client.get('/logout?service=https://www.example.com') # we are redirected to the addresse of the service parameter self.assert_redirect_to_service(client, response) def test_logout_slo(self): """test logout from a service with SLO support""" parameters = [] # test normal SLO # setup a simple one request http server (httpd, host, port) = HttpParamsHandler.run()[0:3] # build a service url depending on which port the http server has binded service = "http://%s:%s" % (host, port) # get a ticket requested by client and being validated by the service (client, ticket) = get_validated_ticket(service)[:2] # the client logout triggering the send of the SLO requests client.get('/logout') # we store the POST parameters send for this ticket for furthur analisys parameters.append((httpd.PARAMS, ticket)) # text SLO with a single_log_out_callback # setup a simple one request http server (httpd, host, port) = HttpParamsHandler.run()[0:3] # set the default test service pattern to use the http server port for SLO requests. # in fact, this single_log_out_callback parametter is usefull to implement SLO # for non http service like imap or ftp self.service_pattern.single_log_out_callback = "http://%s:%s" % (host, port) self.service_pattern.save() # get a ticket requested by client and being validated by the service (client, ticket) = get_validated_ticket(self.service)[:2] # the client logout triggering the send of the SLO requests client.get('/logout') # we store the POST parameters send for this ticket for furthur analisys parameters.append((httpd.PARAMS, ticket)) # for earch POST parameters and corresponding ticket for (params, ticket) in parameters: # there is a POST parameter 'logoutRequest' self.assertTrue(b'logoutRequest' in params and params[b'logoutRequest']) # it is a valid xml root = etree.fromstring(params[b'logoutRequest'][0]) # contening a tag self.assertTrue( root.xpath( "//samlp:LogoutRequest", namespaces={"samlp": "urn:oasis:names:tc:SAML:2.0:protocol"} ) ) # with a tag enclosing the value of the ticket session_index = root.xpath( "//samlp:SessionIndex", namespaces={"samlp": "urn:oasis:names:tc:SAML:2.0:protocol"} ) self.assertEqual(len(session_index), 1) self.assertEqual(session_index[0].text, ticket.value) # SLO error are displayed on logout page (client, ticket) = get_validated_ticket(self.service)[:2] # the client logout triggering the send of the SLO requests but # not http server are listening response = client.get('/logout') self.assertTrue(b"Error during service logout" in response.content) @override_settings(CAS_ENABLE_AJAX_AUTH=True) def test_ajax_logout(self): """ test ajax logout. These methods are here, but I do not really see an use case for javascript logout """ # get a client that is authenticated client = get_auth_client() # fetch the logout page with ajax on response = client.get('/logout', HTTP_X_AJAX='on') # we get a json telling us the user is well logged out self.assertEqual(response.status_code, 200) data = json.loads(response.content.decode("utf8")) self.assertEqual(data["status"], "success") self.assertEqual(data["detail"], "logout") self.assertEqual(data['session_nb'], 1) @override_settings(CAS_ENABLE_AJAX_AUTH=True) def test_ajax_logout_all_session(self): """test ajax logout from a random number a sessions""" # fire a random int in [2, 10[ nb_client = random.randint(2, 10) # get this much of logged clients all for the test user clients = [get_auth_client() for i in range(nb_client)] # fetch the logout page with ajax on, requesting to logout from all sessions response = clients[0].get('/logout?all=1', HTTP_X_AJAX='on') # we get a json telling us the user is well logged out and the number of session # the user has being logged out self.assertEqual(response.status_code, 200) data = json.loads(response.content.decode("utf8")) self.assertEqual(data["status"], "success") self.assertEqual(data["detail"], "logout") self.assertEqual(data['session_nb'], nb_client) @override_settings(CAS_REDIRECT_TO_LOGIN_AFTER_LOGOUT=True) def test_redirect_after_logout(self): """Test redirect to login after logout parameter""" # get a client that is authenticated client = get_auth_client() # fetch the logout page response = client.get('/logout') # as CAS_REDIRECT_TO_LOGIN_AFTER_LOGOUT is True, we are redirected to the login page self.assertEqual(response.status_code, 302) if django.VERSION < (1, 9): # pragma: no cover coverage is computed with dango 1.9 self.assertEqual(response["Location"], "http://testserver/login") else: self.assertEqual(response["Location"], "/login") self.assertFalse(client.session.get("username")) self.assertFalse(client.session.get("authenticated")) @override_settings(CAS_REDIRECT_TO_LOGIN_AFTER_LOGOUT=True) def test_redirect_after_logout_to_service(self): """test prevalence of redirect url/service parameter over redirect to login after logout""" # get a client that is authenticated client = get_auth_client() # fetch the logout page with an url parameter response = client.get('/logout?url=https://www.example.com') # we are redirected to the url parameter and not to the login page self.assert_redirect_to_service(client, response) # fetch the logout page with an service parameter response = client.get('/logout?service=https://www.example.com') # we are redirected to the service parameter and not to the login page self.assert_redirect_to_service(client, response) @override_settings(CAS_REDIRECT_TO_LOGIN_AFTER_LOGOUT=True, CAS_ENABLE_AJAX_AUTH=True) def test_ajax_redirect_after_logout(self): """Test ajax redirect to login after logout parameter""" # get a client that is authenticated client = get_auth_client() # fetch the logout page with ajax on response = client.get('/logout', HTTP_X_AJAX='on') # we get a json telling us the user is well logged out. And url key is added to aks for # redirection to the login page self.assertEqual(response.status_code, 200) data = json.loads(response.content.decode("utf8")) self.assertEqual(data["status"], "success") self.assertEqual(data["detail"], "logout") self.assertEqual(data['session_nb'], 1) self.assertEqual(data['url'], '/login') @override_settings(CAS_AUTH_CLASS='cas_server.auth.TestAuthUser') class AuthTestCase(TestCase): """ Test for the auth view, used for external services to validate (user, pass, service) tuples. """ def setUp(self): """preparing test context""" # setting up a default test service url and pattern self.service = 'https://www.example.com' models.ServicePattern.objects.create( name="example", pattern=r"^https://www\.example\.com(/.*)?$" ) @override_settings(CAS_AUTH_SHARED_SECRET='test') def test_auth_view_goodpass(self): """successful request are awsered by yes""" # get a bare client client = Client() # post the the auth view a valid (username, password, service) and the shared secret # to test the user again the service, a user is created in the database for the # current session and is then deleted as the user is not authenticated response = client.post( '/auth', { 'username': settings.CAS_TEST_USER, 'password': settings.CAS_TEST_PASSWORD, 'service': self.service, 'secret': 'test' } ) # as (username, password, service) and the hared secret are valid, we get yes as a response self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b'yes\n') @override_settings(CAS_AUTH_SHARED_SECRET='test') def test_auth_view_goodpass_logged(self): """successful request are awsered by yes, using a logged sessions""" # same as above client = get_auth_client() # to test the user again the service, a user is fetch in the database for the # current session and is NOT deleted as the user is currently logged. # Deleting the user from the database would cause the user to be logged out as # showed in the login tests response = client.post( '/auth', { 'username': settings.CAS_TEST_USER, 'password': settings.CAS_TEST_PASSWORD, 'service': self.service, 'secret': 'test' } ) # as (username, password, service) and the hared secret are valid, we get yes as a response self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b'yes\n') @override_settings(CAS_AUTH_SHARED_SECRET='test') def test_auth_view_badpass(self): """ bag user password => no""" client = Client() response = client.post( '/auth', { 'username': settings.CAS_TEST_USER, 'password': 'badpass', 'service': self.service, 'secret': 'test' } ) self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b'no\n') @override_settings(CAS_AUTH_SHARED_SECRET='test') def test_auth_view_badservice(self): """bad service => no""" client = Client() response = client.post( '/auth', { 'username': settings.CAS_TEST_USER, 'password': settings.CAS_TEST_PASSWORD, 'service': 'https://www.example.org', 'secret': 'test' } ) self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b'no\n') @override_settings(CAS_AUTH_SHARED_SECRET='test') def test_auth_view_badsecret(self): """bad api key => no""" client = Client() response = client.post( '/auth', { 'username': settings.CAS_TEST_USER, 'password': settings.CAS_TEST_PASSWORD, 'service': self.service, 'secret': 'badsecret' } ) self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b'no\n') def test_auth_view_badsettings(self): """api not set => error""" client = Client() response = client.post( '/auth', { 'username': settings.CAS_TEST_USER, 'password': settings.CAS_TEST_PASSWORD, 'service': self.service, 'secret': 'test' } ) self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b"no\nplease set CAS_AUTH_SHARED_SECRET") @override_settings(CAS_AUTH_SHARED_SECRET='test') def test_auth_view_missing_parameter(self): """missing parameter in request => no""" client = Client() params = { 'username': settings.CAS_TEST_USER, 'password': settings.CAS_TEST_PASSWORD, 'service': self.service, 'secret': 'test' } for key in ['username', 'password', 'service']: send_params = params.copy() del send_params[key] response = client.post('/auth', send_params) self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b'no\n') @override_settings(CAS_AUTH_CLASS='cas_server.auth.TestAuthUser') class ValidateTestCase(TestCase): """tests for the validate view""" def setUp(self): """preparing test context""" # setting up a default test service url and pattern self.service = 'https://www.example.com' self.service_pattern = models.ServicePattern.objects.create( name="example", pattern=r"^https://www\.example\.com(/.*)?$" ) models.ReplaceAttributName.objects.create(name="*", service_pattern=self.service_pattern) # setting up a test service and pattern using a multi valued user attribut as username # the first value of the list should be used as username self.service_user_field = "https://user_field.example.com" self.service_pattern_user_field = models.ServicePattern.objects.create( name="user field", pattern=r"^https://user_field\.example\.com(/.*)?$", user_field="alias" ) # setting up a test service and pattern using a single valued user attribut as username self.service_user_field_alt = "https://user_field_alt.example.com" self.service_pattern_user_field_alt = models.ServicePattern.objects.create( name="user field alt", pattern=r"^https://user_field_alt\.example\.com(/.*)?$", user_field="nom" ) def test_validate_view_ok(self): """test for a valid (ticket, service)""" # get a ticket waiting to be validated for self.service ticket = get_user_ticket_request(self.service)[1] # get a bare client client = Client() # calling the validate view with this ticket value and service response = client.get('/validate', {'ticket': ticket.value, 'service': self.service}) # get yes as a response and the test user username self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b'yes\ntest\n') def test_validate_service_renew(self): """test with a valid (ticket, service) asking for auth renewal""" # case 1 client is renewing and service ask for renew response = get_auth_client(renew="True", service=self.service)[1] self.assertEqual(response.status_code, 302) ticket_value = response['Location'].split('ticket=')[-1] # get a bare client client = Client() # requesting validation with a good (ticket, service) response = client.get( '/validate', {'ticket': ticket_value, 'service': self.service, 'renew': 'True'} ) # the validation should succes with username settings.CAS_TEST_USER and transmit self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b'yes\ntest\n') # cas2 client is renewing and service do not ask for renew (client2, response) = get_auth_client(renew="True", service=self.service) self.assertEqual(response.status_code, 302) ticket_value = response['Location'].split('ticket=')[-1] # get a bare client client = Client() # requesting validation with a good (ticket, service) response = client.get( '/validate', {'ticket': ticket_value, 'service': self.service} ) # the validation should succes with username settings.CAS_TEST_USER and transmit self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b'yes\ntest\n') # case 3, client is not renewing and service ask for renew (client is authenticated) response = client2.get("/login", {"service": self.service}) self.assertEqual(response.status_code, 302) ticket_value = response['Location'].split('ticket=')[-1] client = Client() # requesting validation with a good (ticket, service) response = client.get( '/validate', {'ticket': ticket_value, 'service': self.service, 'renew': 'True'} ) # the validation should fail self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b'no\n') def test_validate_view_badservice(self): """test for a valid ticket but bad service""" ticket = get_user_ticket_request(self.service)[1] client = Client() # calling the validate view with this ticket value and another service response = client.get( '/validate', {'ticket': ticket.value, 'service': "https://www.example.org"} ) # the ticket service and validation service do not match, validation should fail self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b'no\n') def test_validate_view_badticket(self): """test for a bad ticket but valid service""" get_user_ticket_request(self.service) client = Client() # calling the validate view with another ticket value and this service response = client.get( '/validate', {'ticket': "%s-RANDOM" % settings.CAS_SERVICE_TICKET_PREFIX, 'service': self.service} ) # as the ticket is bad, validation should fail self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b'no\n') def test_validate_user_field_ok(self): """ test with a good user_field. A bad user_field (that evaluate to False) wont happed cause it is filtered in the login view """ for (service, username) in [ (self.service_user_field, b"demo1"), (self.service_user_field_alt, b"Nymous") ]: ticket = get_user_ticket_request(service)[1] client = Client() response = client.get( '/validate', {'ticket': ticket.value, 'service': service} ) self.assertEqual(response.status_code, 200) # the user attribute is well used as username self.assertEqual(response.content, b'yes\n' + username + b'\n') def test_validate_missing_parameter(self): """test with a missing GET parameter among [service, ticket]""" ticket = get_user_ticket_request(self.service)[1] client = Client() params = {'ticket': ticket.value, 'service': self.service} for key in ['ticket', 'service']: send_params = params.copy() del send_params[key] response = client.get('/validate', send_params) # if the GET request is missing the ticket or # service GET parameter, validation should fail self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b'no\n') @override_settings(CAS_AUTH_CLASS='cas_server.auth.TestAuthUser') class ValidateServiceTestCase(TestCase, XmlContent): """tests for the serviceValidate view""" def setUp(self): """preparing test context""" # for testing SingleLogOut and Proxy GrantingTicket transmission # we need to use a service on localhost were we launch # a simple one request http server self.service = 'http://127.0.0.1:45678' self.service_pattern = models.ServicePattern.objects.create( name="localhost", pattern=r"^https?://127\.0\.0\.1(:[0-9]+)?(/.*)?$", # allow to request PGT by the service proxy_callback=True, # allow to request PT for the service proxy=True ) # tell the service pattern to transmit all the user attributes (* is a joker) models.ReplaceAttributName.objects.create(name="*", service_pattern=self.service_pattern) # test service pattern using the attribute alias as username self.service_user_field = "https://user_field.example.com" self.service_pattern_user_field = models.ServicePattern.objects.create( name="user field", pattern=r"^https://user_field\.example\.com(/.*)?$", user_field="alias" ) # test service pattern using the attribute nom as username self.service_user_field_alt = "https://user_field_alt.example.com" self.service_pattern_user_field_alt = models.ServicePattern.objects.create( name="user field alt", pattern=r"^https://user_field_alt\.example\.com(/.*)?$", user_field="nom" ) # test service pattern only transmiting one single attributes self.service_one_attribute = "https://one_attribute.example.com" self.service_pattern_one_attribute = models.ServicePattern.objects.create( name="one_attribute", pattern=r"^https://one_attribute\.example\.com(/.*)?$" ) models.ReplaceAttributName.objects.create( name="nom", service_pattern=self.service_pattern_one_attribute ) # test service pattern testing attribute name and value replacement self.service_replace_attribute_list = "https://replace_attribute_list.example.com" self.service_pattern_replace_attribute_list = models.ServicePattern.objects.create( name="replace_attribute_list", pattern=r"^https://replace_attribute_list\.example\.com(/.*)?$", ) models.ReplaceAttributValue.objects.create( attribut="alias", pattern="^demo", replace="truc", service_pattern=self.service_pattern_replace_attribute_list ) models.ReplaceAttributName.objects.create( name="alias", replace="ALIAS", service_pattern=self.service_pattern_replace_attribute_list ) self.service_replace_attribute = "https://replace_attribute.example.com" self.service_pattern_replace_attribute = models.ServicePattern.objects.create( name="replace_attribute", pattern=r"^https://replace_attribute\.example\.com(/.*)?$", ) models.ReplaceAttributValue.objects.create( attribut="nom", pattern="N", replace="P", service_pattern=self.service_pattern_replace_attribute ) models.ReplaceAttributName.objects.create( name="nom", replace="NOM", service_pattern=self.service_pattern_replace_attribute ) def test_validate_service_view_ok(self): """test with a valid (ticket, service), the username and all attributes are transmited""" # get a ticket from an authenticated user waiting for validation ticket = get_user_ticket_request(self.service)[1] # get a bare client client = Client() # requesting validation with a good (ticket, service) response = client.get('/serviceValidate', {'ticket': ticket.value, 'service': self.service}) # the validation should succes with username settings.CAS_TEST_USER and transmit # the attributes settings.CAS_TEST_ATTRIBUTES self.assert_success(response, settings.CAS_TEST_USER, settings.CAS_TEST_ATTRIBUTES) def test_validate_proxy(self): """test ProxyTicket validation on /proxyValidate and /serviceValidate""" ticket = get_proxy_ticket(self.service) client = Client() # requesting validation with a good (ticket, service) response = client.get('/proxyValidate', {'ticket': ticket.value, 'service': self.service}) # and it should succeed self.assert_success(response, settings.CAS_TEST_USER, settings.CAS_TEST_ATTRIBUTES) ticket = get_proxy_ticket(self.service) client = Client() # requesting validation with a good (ticket, service) response = client.get('/serviceValidate', {'ticket': ticket.value, 'service': self.service}) # and it should succeed self.assert_error( response, "INVALID_TICKET", ticket.value ) def test_validate_service_renew(self): """test with a valid (ticket, service) asking for auth renewal""" # case 1 client is renewing and service ask for renew response = get_auth_client(renew="True", service=self.service)[1] self.assertEqual(response.status_code, 302) ticket_value = response['Location'].split('ticket=')[-1] # get a bare client client = Client() # requesting validation with a good (ticket, service) response = client.get( '/serviceValidate', {'ticket': ticket_value, 'service': self.service, 'renew': 'True'} ) # the validation should succes with username settings.CAS_TEST_USER and transmit # the attributes settings.CAS_TEST_ATTRIBUTES self.assert_success(response, settings.CAS_TEST_USER, settings.CAS_TEST_ATTRIBUTES) # cas2 client is renewing and service do not ask for renew (client2, response) = get_auth_client(renew="True", service=self.service) self.assertEqual(response.status_code, 302) ticket_value = response['Location'].split('ticket=')[-1] # get a bare client client = Client() # requesting validation with a good (ticket, service) response = client.get( '/serviceValidate', {'ticket': ticket_value, 'service': self.service} ) # the validation should succes with username settings.CAS_TEST_USER and transmit # the attributes settings.CAS_TEST_ATTRIBUTES self.assert_success(response, settings.CAS_TEST_USER, settings.CAS_TEST_ATTRIBUTES) # case 3, client is not renewing and service ask for renew (client is authenticated) response = client2.get("/login", {"service": self.service}) self.assertEqual(response.status_code, 302) ticket_value = response['Location'].split('ticket=')[-1] client = Client() # requesting validation with a good (ticket, service) response = client.get( '/serviceValidate', {'ticket': ticket_value, 'service': self.service, 'renew': 'True'} ) # the validation should fail self.assert_error( response, "INVALID_TICKET", 'ticket not found' ) def test_validate_service_view_ok_one_attribute(self): """ test with a valid (ticket, service), the username and the 'nom' only attribute are transmited """ # get a ticket for a service that transmit only one attribute ticket = get_user_ticket_request(self.service_one_attribute)[1] client = Client() response = client.get( '/serviceValidate', {'ticket': ticket.value, 'service': self.service_one_attribute} ) # the validation should succed, returning settings.CAS_TEST_USER as username and a single # attribute 'nom' self.assert_success( response, settings.CAS_TEST_USER, {'nom': settings.CAS_TEST_ATTRIBUTES['nom']} ) def test_validate_replace_attributes(self): """test with a valid (ticket, service), attributes name and value replacement""" # get a ticket for a service pattern replacing attributes names # nom -> NOM and value nom -> s/^N/P/ for a single valued attribute ticket = get_user_ticket_request(self.service_replace_attribute)[1] client = Client() response = client.get( '/serviceValidate', {'ticket': ticket.value, 'service': self.service_replace_attribute} ) self.assert_success( response, settings.CAS_TEST_USER, {'NOM': 'Pymous'} ) # get a ticket for a service pattern replacing attributes names # alias -> ALIAS and value alias -> s/demo/truc/ for a multi valued attribute ticket = get_user_ticket_request(self.service_replace_attribute_list)[1] client = Client() response = client.get( '/serviceValidate', {'ticket': ticket.value, 'service': self.service_replace_attribute_list} ) self.assert_success( response, settings.CAS_TEST_USER, {'ALIAS': ['truc1', 'truc2']} ) def test_validate_service_view_badservice(self): """test with a valid ticket but a bad service, the validatin should fail""" # get a ticket for service A ticket = get_user_ticket_request(self.service)[1] client = Client() bad_service = "https://www.example.org" # try to validate it for service B response = client.get('/serviceValidate', {'ticket': ticket.value, 'service': bad_service}) # the validation should fail with error code "INVALID_SERVICE" self.assert_error( response, "INVALID_SERVICE", bad_service ) def test_validate_service_view_badticket_goodprefix(self): """ test with a good service but a bad ticket begining with ST-, the validation should fail with the error (INVALID_TICKET, ticket not found) """ get_user_ticket_request(self.service) client = Client() bad_ticket = "%s-RANDOM" % settings.CAS_SERVICE_TICKET_PREFIX response = client.get('/serviceValidate', {'ticket': bad_ticket, 'service': self.service}) self.assert_error( response, "INVALID_TICKET", 'ticket not found' ) def test_validate_service_view_badticket_badprefix(self): """ test with a good service bud a bad ticket not begining with ST-, the validation should fail with the error (INVALID_TICKET, `the ticket`) """ get_user_ticket_request(self.service) client = Client() bad_ticket = "RANDOM" response = client.get('/serviceValidate', {'ticket': bad_ticket, 'service': self.service}) self.assert_error( response, "INVALID_TICKET", bad_ticket ) def test_validate_service_view_ok_pgturl(self): """test the retrieval of a ProxyGrantingTicket""" # start a simple on request http server (httpd, host, port) = HttpParamsHandler.run()[0:3] # construct the service from it service = "http://%s:%s" % (host, port) # get a ticket to be validated ticket = get_user_ticket_request(service)[1] client = Client() # request a PGT ticket then validating the ticket by setting the pgtUrl parameter response = client.get( '/serviceValidate', {'ticket': ticket.value, 'service': service, 'pgtUrl': service} ) # We should have recieved the PGT via a GET request parameter on the simple http server pgt_params = httpd.PARAMS self.assertEqual(response.status_code, 200) root = etree.fromstring(response.content) # the validation response should return a id to match again the request transmitting the PGT pgtiou = root.xpath( "//cas:proxyGrantingTicket", namespaces={'cas': "http://www.yale.edu/tp/cas"} ) self.assertEqual(len(pgtiou), 1) # the matching id for making corresponde one PGT to a validatin response should match self.assertEqual(pgt_params["pgtIou"], pgtiou[0].text) # the PGT is present in the receive GET requests parameters self.assertTrue("pgtId" in pgt_params) def test_validate_service_pgturl_sslerror(self): """test the retrieval of a ProxyGrantingTicket with a SSL error on the pgtUrl""" (host, port) = HttpParamsHandler.run()[1:3] # is fact the service listen on http and not https raisin a SSL Protocol Error # but other SSL/TLS error should behave the same service = "https://%s:%s" % (host, port) ticket = get_user_ticket_request(service)[1] client = Client() response = client.get( '/serviceValidate', {'ticket': ticket.value, 'service': service, 'pgtUrl': service} ) # The pgtUrl is validated: it must be localhost or have valid x509 certificat and # certificat validation should succed. Moreother, pgtUrl should match a service pattern # with proxy_callback set to True self.assert_error( response, "INVALID_PROXY_CALLBACK", ) def test_validate_service_pgturl_404(self): """ test the retrieval on a ProxyGrantingTicket then to pgtUrl return a http error. PGT creation should be aborted but the ticket still be valid """ (host, port) = Http404Handler.run()[1:3] service = "http://%s:%s" % (host, port) ticket = get_user_ticket_request(service)[1] client = Client() response = client.get( '/serviceValidate', {'ticket': ticket.value, 'service': service, 'pgtUrl': service} ) # The ticket is successfully validated root = self.assert_success(response, settings.CAS_TEST_USER, settings.CAS_TEST_ATTRIBUTES) # but no PGT is transmitted pgtiou = root.xpath( "//cas:proxyGrantingTicket", namespaces={'cas': "http://www.yale.edu/tp/cas"} ) self.assertFalse(pgtiou) def test_validate_service_pgturl_bad_proxy_callback(self): """test the retrieval of a ProxyGrantingTicket, not allowed pgtUrl should be denied""" self.service_pattern.proxy_callback = False self.service_pattern.save() ticket = get_user_ticket_request(self.service)[1] client = Client() response = client.get( '/serviceValidate', {'ticket': ticket.value, 'service': self.service, 'pgtUrl': self.service} ) self.assert_error( response, "INVALID_PROXY_CALLBACK", "callback url not allowed by configuration" ) self.service_pattern.proxy_callback = True ticket = get_user_ticket_request(self.service)[1] client = Client() response = client.get( '/serviceValidate', {'ticket': ticket.value, 'service': self.service, 'pgtUrl': "https://www.example.org"} ) self.assert_error( response, "INVALID_PROXY_CALLBACK", "callback url not allowed by configuration" ) def test_validate_user_field_ok(self): """ test with a good user_field. A bad user_field (that evaluate to False) wont happed cause it is filtered in the login view """ for (service, username) in [ (self.service_user_field, settings.CAS_TEST_ATTRIBUTES["alias"][0]), (self.service_user_field_alt, settings.CAS_TEST_ATTRIBUTES["nom"]) ]: # requesting a ticket for a service url matched by a service pattern using a user # attribute as username ticket = get_user_ticket_request(service)[1] client = Client() response = client.get( '/serviceValidate', {'ticket': ticket.value, 'service': service} ) # The validate shoudl be successful with specified username and no attributes transmited self.assert_success( response, username, {} ) def test_validate_missing_parameter(self): """test with a missing GET parameter among [service, ticket]""" ticket = get_user_ticket_request(self.service)[1] client = Client() params = {'ticket': ticket.value, 'service': self.service} for key in ['ticket', 'service']: send_params = params.copy() del send_params[key] response = client.get('/serviceValidate', send_params) # a validation request with a missing GET parameter should fail self.assert_error( response, "INVALID_REQUEST", "you must specify a service and a ticket" ) @override_settings(CAS_AUTH_CLASS='cas_server.auth.TestAuthUser') class ProxyTestCase(TestCase, BaseServicePattern, XmlContent): """tests for the proxy view""" def setUp(self): """preparing test context""" # we prepare a bunch a service url and service patterns for tests self.setup_service_patterns(proxy=True) # set the default service pattern to localhost to be able to retrieve PGT self.service = 'http://127.0.0.1' self.service_pattern = models.ServicePattern.objects.create( name="localhost", pattern=r"^http://127\.0\.0\.1(:[0-9]+)?(/.*)?$", proxy=True, proxy_callback=True ) # transmit all attributes models.ReplaceAttributName.objects.create(name="*", service_pattern=self.service_pattern) def test_validate_proxy_ok(self): """ Get a PGT, get a proxy ticket, validate it. Validation should succeed and show the proxy service URL. """ # we directrly get a ProxyGrantingTicket params = get_pgt() # We try get a proxy ticket with our PGT client1 = Client() # for what we send a GET request to /proxy with ge PGT and the target service for which # we want a ProxyTicket to. response = client1.get( '/proxy', {'pgt': params['pgtId'], 'targetService': "https://www.example.com"} ) self.assertEqual(response.status_code, 200) # we should sucessfully reteive a PT root = etree.fromstring(response.content) sucess = root.xpath("//cas:proxySuccess", namespaces={'cas': "http://www.yale.edu/tp/cas"}) self.assertTrue(sucess) proxy_ticket = root.xpath( "//cas:proxyTicket", namespaces={'cas': "http://www.yale.edu/tp/cas"} ) self.assertEqual(len(proxy_ticket), 1) proxy_ticket = proxy_ticket[0].text # validate the proxy ticket with the service for which is was emitted client2 = Client() response = client2.get( '/proxyValidate', {'ticket': proxy_ticket, 'service': "https://www.example.com"} ) # validation should succeed and return settings.CAS_TEST_USER as username # and settings.CAS_TEST_ATTRIBUTES as attributes root = self.assert_success( response, settings.CAS_TEST_USER, settings.CAS_TEST_ATTRIBUTES ) # in the PT validation response, it should have the service url of the PGY proxies = root.xpath("//cas:proxies", namespaces={'cas': "http://www.yale.edu/tp/cas"}) self.assertEqual(len(proxies), 1) proxy = proxies[0].xpath("//cas:proxy", namespaces={'cas': "http://www.yale.edu/tp/cas"}) self.assertEqual(len(proxy), 1) self.assertEqual(proxy[0].text, params["service"]) def test_validate_proxy_bad_pgt(self): """Try to get a ProxyTicket with a bad PGT. The PT generation should fail""" # we directrly get a ProxyGrantingTicket params = get_pgt() client = Client() response = client.get( '/proxy', { 'pgt': "%s-RANDOM" % settings.CAS_PROXY_GRANTING_TICKET_PREFIX, 'targetService': params['service'] } ) self.assert_error( response, "INVALID_TICKET", "PGT %s-RANDOM not found" % settings.CAS_PROXY_GRANTING_TICKET_PREFIX ) def test_validate_proxy_bad_service(self): """ Try to get a ProxyTicket for a denied service and a service that do not allow PT. The PT generation should fail. """ # we directrly get a ProxyGrantingTicket params = get_pgt() # try to get a PT for a denied service client1 = Client() response = client1.get( '/proxy', {'pgt': params['pgtId'], 'targetService': "https://www.example.org"} ) self.assert_error( response, "UNAUTHORIZED_SERVICE", "https://www.example.org" ) # try to get a PT for a service that do not allow PT self.service_pattern.proxy = False self.service_pattern.save() client2 = Client() response = client2.get( '/proxy', {'pgt': params['pgtId'], 'targetService': params['service']} ) self.assert_error( response, "UNAUTHORIZED_SERVICE", 'the service %s does not allow proxy tickets' % params['service'] ) self.service_pattern.proxy = True self.service_pattern.save() def test_proxy_unauthorized_user(self): """ Try to get a PT for services that do not allow the current user: * first with a service that restrict allowed username * second with a service requiring somes conditions on the user attributes * third with a service using a particular user attribute as username All this tests should fail """ # we directrly get a ProxyGrantingTicket params = get_pgt() for service in [ # do ot allow the test username self.service_restrict_user_fail, # require the 'nom' attribute to be 'toto' self.service_filter_fail, # want to use the non-exitant 'uid' attribute as username self.service_field_needed_fail ]: client = Client() response = client.get( '/proxy', {'pgt': params['pgtId'], 'targetService': service} ) # PT generation should fail self.assert_error( response, "UNAUTHORIZED_USER", 'User %s not allowed on %s' % (settings.CAS_TEST_USER, service) ) def test_proxy_missing_parameter(self): """Try to get a PGT with some missing GET parameters. The PT should not be emited""" params = get_pgt() base_params = {'pgt': params['pgtId'], 'targetService': "https://www.example.org"} for key in ["pgt", 'targetService']: send_params = base_params.copy() del send_params[key] client = Client() response = client.get("/proxy", send_params) self.assert_error( response, "INVALID_REQUEST", 'you must specify and pgt and targetService' ) @override_settings(CAS_AUTH_CLASS='cas_server.auth.TestAuthUser') class SamlValidateTestCase(TestCase, BaseServicePattern, XmlContent): """tests for the proxy view""" def setUp(self): """preparing test context""" # we prepare a bunch a service url and service patterns for tests self.setup_service_patterns(proxy=True) # special service pattern for retrieving a PGT self.service_pgt = 'http://127.0.0.1' self.service_pattern_pgt = models.ServicePattern.objects.create( name="localhost", pattern=r"^http://127\.0\.0\.1(:[0-9]+)?(/.*)?$", proxy=True, proxy_callback=True ) models.ReplaceAttributName.objects.create( name="*", service_pattern=self.service_pattern_pgt ) # template for the XML POST need to be send to validate a ticket using SAML 1.1 xml_template = """ %(ticket)s """ def assert_success(self, response, username, original_attributes): """assert ticket validation success""" self.assertEqual(response.status_code, 200) # on validation success, the response should have a StatusCode set to Success root = etree.fromstring(response.content) success = root.xpath( "//samlp:StatusCode", namespaces={'samlp': "urn:oasis:names:tc:SAML:1.0:protocol"} ) self.assertEqual(len(success), 1) self.assertTrue(success[0].attrib['Value'].endswith(":Success")) # the user username should be return whithin tags user = root.xpath( "//samla:NameIdentifier", namespaces={'samla': "urn:oasis:names:tc:SAML:1.0:assertion"} ) self.assertTrue(user) self.assertEqual(user[0].text, username) # the returned attributes should match original_attributes attributes = root.xpath( "//samla:AttributeStatement/samla:Attribute", namespaces={'samla': "urn:oasis:names:tc:SAML:1.0:assertion"} ) ignore_attrs = { "authenticationDate", "longTermAuthenticationRequestTokenUsed", "isFromNewLogin" } - set(original_attributes.keys()) attrs = set() for attr in attributes: if not attr.attrib['AttributeName'] in ignore_attrs: attrs.add((attr.attrib['AttributeName'], attr.getchildren()[0].text)) original = set() for key, value in original_attributes.items(): if isinstance(value, list): for subval in value: original.add((key, subval)) else: original.add((key, value)) self.assertEqual(original, attrs) def assert_error(self, response, code, msg=None): """assert ticket validation error""" self.assertEqual(response.status_code, 200) # on error the status code value should be the one provider in `code` root = etree.fromstring(response.content) error = root.xpath( "//samlp:StatusCode", namespaces={'samlp': "urn:oasis:names:tc:SAML:1.0:protocol"} ) self.assertEqual(len(error), 1) self.assertTrue(error[0].attrib['Value'].endswith(":%s" % code)) # it may have an error message if msg is not None: self.assertEqual(error[0].text, msg) def test_saml_ok(self): """ test with a valid (ticket, service), with a ST and a PT, the username and all attributes are transmited""" tickets = [ # return a ServiceTicket (standard ticket) waiting for validation get_user_ticket_request(self.service)[1], # return a PT waiting for validation get_proxy_ticket(self.service) ] for ticket in tickets: client = Client() # we send the POST validation requests response = client.post( '/samlValidate?TARGET=%s' % self.service, self.xml_template % { 'ticket': ticket.value, 'request_id': utils.gen_saml_id(), 'issue_instant': timezone.now().isoformat() }, content_type="text/xml; encoding='utf-8'" ) # and it should succeed self.assert_success(response, settings.CAS_TEST_USER, settings.CAS_TEST_ATTRIBUTES) def test_saml_ok_user_field(self): """test with a valid(ticket, service), use a attributes as transmitted username""" for (service, username) in [ (self.service_field_needed_success, settings.CAS_TEST_ATTRIBUTES['alias'][0]), (self.service_field_needed_success_alt, settings.CAS_TEST_ATTRIBUTES['nom']) ]: ticket = get_user_ticket_request(service)[1] client = Client() response = client.post( '/samlValidate?TARGET=%s' % service, self.xml_template % { 'ticket': ticket.value, 'request_id': utils.gen_saml_id(), 'issue_instant': timezone.now().isoformat() }, content_type="text/xml; encoding='utf-8'" ) self.assert_success(response, username, {}) def test_saml_bad_ticket(self): """test validation with a bad ST and a bad PT, validation should fail""" tickets = [utils.gen_st(), utils.gen_pt()] for ticket in tickets: client = Client() response = client.post( '/samlValidate?TARGET=%s' % self.service, self.xml_template % { 'ticket': ticket, 'request_id': utils.gen_saml_id(), 'issue_instant': timezone.now().isoformat() }, content_type="text/xml; encoding='utf-8'" ) self.assert_error( response, "AuthnFailed", 'ticket %s not found' % ticket ) def test_saml_bad_ticket_prefix(self): """test validation with a bad ticket prefix. Validation should fail with 'AuthnFailed'""" bad_ticket = "RANDOM-NOT-BEGINING-WITH-ST-OR-ST" client = Client() response = client.post( '/samlValidate?TARGET=%s' % self.service, self.xml_template % { 'ticket': bad_ticket, 'request_id': utils.gen_saml_id(), 'issue_instant': timezone.now().isoformat() }, content_type="text/xml; encoding='utf-8'" ) self.assert_error( response, "AuthnFailed", 'ticket %s should begin with PT- or ST-' % bad_ticket ) def test_saml_bad_target(self): """test with a valid ticket, but using a bad target, validation should fail""" bad_target = "https://www.example.org" ticket = get_user_ticket_request(self.service)[1] client = Client() response = client.post( '/samlValidate?TARGET=%s' % bad_target, self.xml_template % { 'ticket': ticket.value, 'request_id': utils.gen_saml_id(), 'issue_instant': timezone.now().isoformat() }, content_type="text/xml; encoding='utf-8'" ) self.assert_error( response, "AuthnFailed", 'TARGET %s does not match ticket service' % bad_target ) def test_saml_bad_xml(self): """test validation with a bad xml request, validation should fail""" client = Client() response = client.post( '/samlValidate?TARGET=%s' % self.service, "", content_type="text/xml; encoding='utf-8'" ) self.assert_error(response, 'VersionMismatch') django-cas-server-1.1.0/cas_server/management/0000755000175000017500000000000013436457571022774 5ustar valentinvalentin00000000000000django-cas-server-1.1.0/cas_server/management/__init__.py0000644000175000017500000000000012632536713025064 0ustar valentinvalentin00000000000000django-cas-server-1.1.0/cas_server/management/commands/0000755000175000017500000000000013436457571024575 5ustar valentinvalentin00000000000000django-cas-server-1.1.0/cas_server/management/commands/cas_clean_sessions.py0000644000175000017500000000175413060003340030762 0ustar valentinvalentin00000000000000# -*- coding: utf-8 -*- # 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 General Public License version 3 for # more details. # # You should have received a copy of the GNU General Public License version 3 # along with this program; if not, write to the Free Software Foundation, Inc., 51 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # (c) 2016 Valentin Samir """Clean deleted sessions management command""" from django.core.management.base import BaseCommand from django.utils.translation import ugettext_lazy as _ from ... import models class Command(BaseCommand): """Clean deleted sessions""" args = '' help = _(u"Clean deleted sessions") def handle(self, *args, **options): models.User.clean_deleted_sessions() models.UserAttributes.clean_old_entries() models.NewVersionWarning.send_mails() django-cas-server-1.1.0/cas_server/management/commands/__init__.py0000644000175000017500000000000012632536713026665 0ustar valentinvalentin00000000000000django-cas-server-1.1.0/cas_server/management/commands/cas_clean_federate.py0000644000175000017500000000156612757022243030713 0ustar valentinvalentin00000000000000# -*- coding: utf-8 -*- # 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 General Public License version 3 for # more details. # # You should have received a copy of the GNU General Public License version 3 # along with this program; if not, write to the Free Software Foundation, Inc., 51 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # (c) 2016 Valentin Samir from django.core.management.base import BaseCommand from django.utils.translation import ugettext_lazy as _ from ... import models class Command(BaseCommand): args = '' help = _(u"Clean old federated users") def handle(self, *args, **options): models.FederatedUser.clean_old_entries() models.FederateSLO.clean_deleted_sessions() django-cas-server-1.1.0/cas_server/management/commands/cas_clean_tickets.py0000644000175000017500000000201312757406463030600 0ustar valentinvalentin00000000000000# -*- coding: utf-8 -*- # 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 General Public License version 3 for # more details. # # You should have received a copy of the GNU General Public License version 3 # along with this program; if not, write to the Free Software Foundation, Inc., 51 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # (c) 2016 Valentin Samir """Clean old trickets management command""" from django.core.management.base import BaseCommand from django.utils.translation import ugettext_lazy as _ from ... import models class Command(BaseCommand): """Clean old trickets""" args = '' help = _(u"Clean old tickets") def handle(self, *args, **options): models.User.clean_old_entries() for ticket_class in [models.ServiceTicket, models.ProxyTicket, models.ProxyGrantingTicket]: ticket_class.clean_old_entries() django-cas-server-1.1.0/cas_server/auth.py0000644000175000017500000004777213436457341022207 0ustar valentinvalentin00000000000000# -*- coding: utf-8 -*- # 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 General Public License version 3 for # more details. # # You should have received a copy of the GNU General Public License version 3 # along with this program; if not, write to the Free Software Foundation, Inc., 51 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # (c) 2015-2016 Valentin Samir """Some authentication classes for the CAS""" from django.conf import settings from django.contrib.auth import get_user_model from django.utils import timezone from django.db import connections, DatabaseError import warnings from datetime import timedelta from six.moves import range try: # pragma: no cover import MySQLdb import MySQLdb.cursors except ImportError: MySQLdb = None try: # pragma: no cover import ldap3 import ldap3.core.exceptions except ImportError: ldap3 = None from .models import FederatedUser, UserAttributes from .utils import check_password, dictfetchall class AuthUser(object): """ Authentication base class :param unicode username: A username, stored in the :attr:`username` class attribute. """ #: username used to instanciate the current object username = None def __init__(self, username): self.username = username def test_password(self, password): """ Tests ``password`` against the user-supplied password. :raises NotImplementedError: always. The method need to be implemented by subclasses """ raise NotImplementedError() def attributs(self): """ The user attributes. raises NotImplementedError: always. The method need to be implemented by subclasses """ raise NotImplementedError() class DummyAuthUser(AuthUser): # pragma: no cover """ A Dummy authentication class. Authentication always fails :param unicode username: A username, stored in the :attr:`username` class attribute. There is no valid value for this attribute here. """ def test_password(self, password): """ Tests ``password`` against the user-supplied password. :param unicode password: a clear text password as submited by the user. :return: always ``False`` :rtype: bool """ return False def attributs(self): """ The user attributes. :return: en empty :class:`dict`. :rtype: dict """ return {} class TestAuthUser(AuthUser): """ A test authentication class only working for one unique user. :param unicode username: A username, stored in the :attr:`username` class attribute. The uniq valid value is ``settings.CAS_TEST_USER``. """ def test_password(self, password): """ Tests ``password`` against the user-supplied password. :param unicode password: a clear text password as submited by the user. :return: ``True`` if :attr:`username` is valid and ``password`` is equal to ``settings.CAS_TEST_PASSWORD``, ``False`` otherwise. :rtype: bool """ return self.username == settings.CAS_TEST_USER and password == settings.CAS_TEST_PASSWORD def attributs(self): """ The user attributes. :return: the ``settings.CAS_TEST_ATTRIBUTES`` :class:`dict` if :attr:`username` is valid, an empty :class:`dict` otherwise. :rtype: dict """ if self.username == settings.CAS_TEST_USER: return settings.CAS_TEST_ATTRIBUTES else: # pragma: no cover (should not happen) return {} class DBAuthUser(AuthUser): # pragma: no cover """base class for databate based auth classes""" #: DB user attributes as a :class:`dict` if the username is found in the database. user = None def attributs(self): """ The user attributes. :return: a :class:`dict` with the user attributes. Attributes may be :func:`unicode` or :class:`list` of :func:`unicode`. If the user do not exists, the returned :class:`dict` is empty. :rtype: dict """ if self.user: return self.user else: return {} class MysqlAuthUser(DBAuthUser): # pragma: no cover """ DEPRECATED, use :class:`SqlAuthUser` instead. A mysql authentication class: authenticate user against a mysql database :param unicode username: A username, stored in the :attr:`username` class attribute. Valid value are fetched from the MySQL database set with ``settings.CAS_SQL_*`` settings parameters using the query ``settings.CAS_SQL_USER_QUERY``. """ def __init__(self, username): warnings.warn( ( "MysqlAuthUser authentication class is deprecated: " "use cas_server.auth.SqlAuthUser instead" ), UserWarning ) # see the connect function at # http://mysql-python.sourceforge.net/MySQLdb.html#functions-and-attributes # for possible mysql config parameters. mysql_config = { "user": settings.CAS_SQL_USERNAME, "passwd": settings.CAS_SQL_PASSWORD, "db": settings.CAS_SQL_DBNAME, "host": settings.CAS_SQL_HOST, "charset": settings.CAS_SQL_DBCHARSET, "cursorclass": MySQLdb.cursors.DictCursor } if not MySQLdb: raise RuntimeError("Please install MySQLdb before using the MysqlAuthUser backend") conn = MySQLdb.connect(**mysql_config) curs = conn.cursor() if curs.execute(settings.CAS_SQL_USER_QUERY, (username,)) == 1: self.user = curs.fetchone() super(MysqlAuthUser, self).__init__(self.user['username']) else: super(MysqlAuthUser, self).__init__(username) def test_password(self, password): """ Tests ``password`` against the user-supplied password. :param unicode password: a clear text password as submited by the user. :return: ``True`` if :attr:`username` is valid and ``password`` is correct, ``False`` otherwise. :rtype: bool """ if self.user: return check_password( settings.CAS_SQL_PASSWORD_CHECK, password, self.user["password"], settings.CAS_SQL_DBCHARSET ) else: return False class SqlAuthUser(DBAuthUser): # pragma: no cover """ A SQL authentication class: authenticate user against a SQL database. The SQL database must be configures in settings.py as ``settings.DATABASES['cas_server']``. :param unicode username: A username, stored in the :attr:`username` class attribute. Valid value are fetched from the MySQL database set with ``settings.CAS_SQL_*`` settings parameters using the query ``settings.CAS_SQL_USER_QUERY``. """ def __init__(self, username): if "cas_server" not in connections: raise RuntimeError("Please configure the 'cas_server' database in settings.DATABASES") for retry_nb in range(3): try: with connections["cas_server"].cursor() as curs: curs.execute(settings.CAS_SQL_USER_QUERY, (username,)) results = dictfetchall(curs) if len(results) == 1: self.user = results[0] super(SqlAuthUser, self).__init__(self.user['username']) else: super(SqlAuthUser, self).__init__(username) break except DatabaseError: connections["cas_server"].close() if retry_nb == 2: raise def test_password(self, password): """ Tests ``password`` against the user-supplied password. :param unicode password: a clear text password as submited by the user. :return: ``True`` if :attr:`username` is valid and ``password`` is correct, ``False`` otherwise. :rtype: bool """ if self.user: return check_password( settings.CAS_SQL_PASSWORD_CHECK, password, self.user["password"], settings.CAS_SQL_PASSWORD_CHARSET ) else: return False class LdapAuthUser(DBAuthUser): # pragma: no cover """ A ldap authentication class: authenticate user against a ldap database :param unicode username: A username, stored in the :attr:`username` class attribute. Valid value are fetched from the ldap database set with ``settings.CAS_LDAP_*`` settings parameters. """ _conn = None @classmethod def get_conn(cls): """Return a connection object to the ldap database""" conn = cls._conn if conn is None or conn.closed: conn = ldap3.Connection( settings.CAS_LDAP_SERVER, settings.CAS_LDAP_USER, settings.CAS_LDAP_PASSWORD, client_strategy="RESTARTABLE", auto_bind=True ) cls._conn = conn return conn def __init__(self, username): if not ldap3: raise RuntimeError("Please install ldap3 before using the LdapAuthUser backend") if not settings.CAS_LDAP_BASE_DN: raise ValueError( "You must define CAS_LDAP_BASE_DN for using the ldap authentication backend" ) # in case we got deconnected from the database, retry to connect 2 times for retry_nb in range(3): try: conn = self.get_conn() if conn.search( settings.CAS_LDAP_BASE_DN, settings.CAS_LDAP_USER_QUERY % ldap3.utils.conv.escape_bytes(username), attributes=ldap3.ALL_ATTRIBUTES ) and len(conn.entries) == 1: # try the new ldap3>=2 API try: user = conn.entries[0].entry_attributes_as_dict # store the user dn user["dn"] = conn.entries[0].entry_dn # fallback to ldap3<2 API except ( ldap3.core.exceptions.LDAPKeyError, # ldap3<1 exception ldap3.core.exceptions.LDAPAttributeError # ldap3<2 exception ): user = conn.entries[0].entry_get_attributes_dict() # store the user dn user["dn"] = conn.entries[0].entry_get_dn() if user.get(settings.CAS_LDAP_USERNAME_ATTR): self.user = user super(LdapAuthUser, self).__init__(user[settings.CAS_LDAP_USERNAME_ATTR][0]) else: super(LdapAuthUser, self).__init__(username) else: super(LdapAuthUser, self).__init__(username) break except ldap3.core.exceptions.LDAPCommunicationError: if retry_nb == 2: raise def test_password(self, password): """ Tests ``password`` against the user-supplied password. :param unicode password: a clear text password as submited by the user. :return: ``True`` if :attr:`username` is valid and ``password`` is correct, ``False`` otherwise. :rtype: bool """ if self.user and settings.CAS_LDAP_PASSWORD_CHECK == "bind": try: conn = ldap3.Connection( settings.CAS_LDAP_SERVER, self.user["dn"], password, auto_bind=True ) try: # fetch the user attribute if conn.search( settings.CAS_LDAP_BASE_DN, settings.CAS_LDAP_USER_QUERY % ldap3.utils.conv.escape_bytes(self.username), attributes=ldap3.ALL_ATTRIBUTES ) and len(conn.entries) == 1: # try the ldap3>=2 API try: attributes = conn.entries[0].entry_attributes_as_dict # store the user dn attributes["dn"] = conn.entries[0].entry_dn # fallback to ldap<2 API except ( ldap3.core.exceptions.LDAPKeyError, # ldap3<1 exception ldap3.core.exceptions.LDAPAttributeError # ldap3<2 exception ): attributes = conn.entries[0].entry_get_attributes_dict() attributes["dn"] = conn.entries[0].entry_get_dn() # cache the attributes locally as we wont have access to the user password # later. user = UserAttributes.objects.get_or_create(username=self.username)[0] user.attributs = attributes user.save() finally: conn.unbind() return True except ( ldap3.core.exceptions.LDAPBindError, ldap3.core.exceptions.LDAPCommunicationError ): return False elif self.user and self.user.get(settings.CAS_LDAP_PASSWORD_ATTR): return check_password( settings.CAS_LDAP_PASSWORD_CHECK, password, self.user[settings.CAS_LDAP_PASSWORD_ATTR][0], settings.CAS_LDAP_PASSWORD_CHARSET ) else: return False def attributs(self): """ The user attributes. :return: a :class:`dict` with the user attributes. Attributes may be :func:`unicode` or :class:`list` of :func:`unicode`. If the user do not exists, the returned :class:`dict` is empty. :rtype: dict :raises NotImplementedError: if the password check method in `CAS_LDAP_PASSWORD_CHECK` do not allow to fetch the attributes without the user credentials. """ if settings.CAS_LDAP_PASSWORD_CHECK == "bind": raise NotImplementedError() else: return super(LdapAuthUser, self).attributs() class DjangoAuthUser(AuthUser): # pragma: no cover """ A django auth class: authenticate user against django internal users :param unicode username: A username, stored in the :attr:`username` class attribute. Valid value are usernames of django internal users. """ #: a django user object if the username is found. The user model is retreived #: using :func:`django.contrib.auth.get_user_model`. user = None def __init__(self, username): User = get_user_model() try: self.user = User.objects.get(username=username) except User.DoesNotExist: pass super(DjangoAuthUser, self).__init__(username) def test_password(self, password): """ Tests ``password`` against the user-supplied password. :param unicode password: a clear text password as submited by the user. :return: ``True`` if :attr:`user` is valid and ``password`` is correct, ``False`` otherwise. :rtype: bool """ if self.user: return self.user.check_password(password) else: return False def attributs(self): """ The user attributes, defined as the fields on the :attr:`user` object. :return: a :class:`dict` with the :attr:`user` object fields. Attributes may be If the user do not exists, the returned :class:`dict` is empty. :rtype: dict """ if self.user: attr = {} # _meta.get_fields() is from the new documented _meta interface in django 1.8 try: field_names = [ field.attname for field in self.user._meta.get_fields() if hasattr(field, "attname") ] # backward compatibility with django 1.7 except AttributeError: # pragma: no cover (only used by django 1.7) field_names = self.user._meta.get_all_field_names() for name in field_names: attr[name] = getattr(self.user, name) # unfold user_permissions many to many relation if 'user_permissions' in attr: attr['user_permissions'] = [ ( u"%s.%s" % ( perm.content_type.model_class().__module__, perm.content_type.model_class().__name__ ), perm.codename ) for perm in attr['user_permissions'].filter() ] # unfold group many to many relation if 'groups' in attr: attr['groups'] = [group.name for group in attr['groups'].filter()] return attr else: return {} class CASFederateAuth(AuthUser): """ Authentication class used then CAS_FEDERATE is True :param unicode username: A username, stored in the :attr:`username` class attribute. Valid value are usernames of :class:`FederatedUser` object. :class:`FederatedUser` object are created on CAS backends successful ticket validation. """ #: a :class`FederatedUser` object if ``username`` is found. user = None def __init__(self, username): try: self.user = FederatedUser.get_from_federated_username(username) super(CASFederateAuth, self).__init__( self.user.federated_username ) except FederatedUser.DoesNotExist: super(CASFederateAuth, self).__init__(username) def test_password(self, ticket): """ Tests ``password`` against the user-supplied password. :param unicode password: The CAS tickets just used to validate the user authentication against its CAS backend. :return: ``True`` if :attr:`user` is valid and ``password`` is a ticket validated less than ``settings.CAS_TICKET_VALIDITY`` secondes and has not being previously used for authenticated this :class:`FederatedUser`. ``False`` otherwise. :rtype: bool """ if not self.user or not self.user.ticket: return False else: return ( ticket == self.user.ticket and self.user.last_update > (timezone.now() - timedelta(seconds=settings.CAS_TICKET_VALIDITY)) ) def attributs(self): """ The user attributes, as returned by the CAS backend. :return: :obj:`FederatedUser.attributs`. If the user do not exists, the returned :class:`dict` is empty. :rtype: dict """ if not self.user: # pragma: no cover (should not happen) return {} else: return self.user.attributs django-cas-server-1.1.0/cas_server/templatetags/0000755000175000017500000000000013436457571023352 5ustar valentinvalentin00000000000000django-cas-server-1.1.0/cas_server/templatetags/__init__.py0000644000175000017500000000000012757022243025436 0ustar valentinvalentin00000000000000django-cas-server-1.1.0/cas_server/templatetags/cas_server.py0000644000175000017500000000242412757022243026047 0ustar valentinvalentin00000000000000# 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 General Public License version 3 for # more details. # # You should have received a copy of the GNU General Public License version 3 # along with this program; if not, write to the Free Software Foundation, Inc., 51 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # (c) 2015-2016 Valentin Samir """template tags for the app""" from django import template from django import forms register = template.Library() @register.filter(name='is_checkbox') def is_checkbox(field): """ check if a form bound field is a checkbox :param django.forms.BoundField field: A bound field :return: ``True`` if the field is a checkbox, ``False`` otherwise. :rtype: bool """ return isinstance(field.field.widget, forms.CheckboxInput) @register.filter(name='is_hidden') def is_hidden(field): """ check if a form bound field is hidden :param django.forms.BoundField field: A bound field :return: ``True`` if the field is hidden, ``False`` otherwise. :rtype: bool """ return isinstance(field.field.widget, forms.HiddenInput) django-cas-server-1.1.0/cas_server/migrations/0000755000175000017500000000000013436457571023034 5ustar valentinvalentin00000000000000django-cas-server-1.1.0/cas_server/migrations/__init__.py0000644000175000017500000000000012632536713025124 0ustar valentinvalentin00000000000000django-cas-server-1.1.0/cas_server/migrations/0001_squashed_0013_auto_20170329_1748.py0000644000175000017500000006206413416345433031027 0ustar valentinvalentin00000000000000# -*- coding: utf-8 -*- # Generated by Django 1.10.8 on 2018-04-29 17:40 from __future__ import unicode_literals import cas_server.utils from django.db import migrations, models import django.db.models.deletion import django.utils.timezone class Migration(migrations.Migration): replaces = [('cas_server', '0001_squashed_0021_auto_20150611_2102'), ('cas_server', '0002_auto_20151212_1300'), ('cas_server', '0003_auto_20151212_1721'), ('cas_server', '0004_auto_20151218_1032'), ('cas_server', '0005_auto_20160616_1018'), ('cas_server', '0006_auto_20160706_1727'), ('cas_server', '0007_auto_20160723_2252'), ('cas_server', '0008_newversionwarning'), ('cas_server', '0009_auto_20160814_0619'), ('cas_server', '0010_auto_20160824_2112'), ('cas_server', '0011_auto_20161007_1258'), ('cas_server', '0012_auto_20170328_1610'), ('cas_server', '0013_auto_20170329_1748')] initial = True dependencies = [ ('sessions', '0001_initial'), ] operations = [ migrations.CreateModel( name='Proxy', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('url', models.CharField(max_length=255)), ], options={ 'ordering': ('-pk',), }, ), migrations.CreateModel( name='ProxyGrantingTicket', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('attributs', models.TextField(blank=True, default=None, null=True)), ('validate', models.BooleanField(default=False)), ('service', models.TextField()), ('creation', models.DateTimeField(auto_now_add=True)), ('renew', models.BooleanField(default=False)), ('value', models.CharField(default=cas_server.utils.gen_pgt, max_length=255, unique=True)), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='ProxyTicket', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('attributs', models.TextField(blank=True, default=None, null=True)), ('validate', models.BooleanField(default=False)), ('service', models.TextField()), ('creation', models.DateTimeField(auto_now_add=True)), ('renew', models.BooleanField(default=False)), ('value', models.CharField(default=cas_server.utils.gen_pt, max_length=255, unique=True)), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='ServicePattern', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('pos', models.IntegerField(default=100)), ('pattern', models.CharField(max_length=255, unique=True)), ('user_field', models.CharField(blank=True, default=b'', help_text=b"Nom de l'attribut transmit comme username, vide = login", max_length=255)), ('usernames', models.CharField(blank=True, default=b'', help_text=b"Liste d'utilisateurs accept\xc3\xa9s s\xc3\xa9par\xc3\xa9 par des virgules, vide = tous les utilisateur", max_length=255)), ('attributs', models.CharField(blank=True, default=b'', help_text=b"Liste des nom d'attributs \xc3\xa0 transmettre au service, s\xc3\xa9par\xc3\xa9 par une virgule. vide = aucun", max_length=255)), ('proxy', models.BooleanField(default=False, help_text=b"Un ProxyGrantingTicket peut \xc3\xaatre d\xc3\xa9livr\xc3\xa9 au service pour s'authentifier en temps que l'utilisateur sur d'autres services")), ('filter', models.CharField(blank=True, default=b'', help_text=b'Une lambda fonction pour filtrer sur les utilisateur o\xc3\xb9 leurs attribut, arg1: username, arg2:attrs_dict. vide = pas de filtre', max_length=255)), ], options={ 'ordering': ('pos',), }, ), migrations.CreateModel( name='ServiceTicket', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('attributs', models.TextField(blank=True, default=None, null=True)), ('validate', models.BooleanField(default=False)), ('service', models.TextField()), ('creation', models.DateTimeField(auto_now_add=True)), ('renew', models.BooleanField(default=False)), ('value', models.CharField(default=cas_server.utils.gen_st, max_length=255, unique=True)), ], options={ 'abstract': False, }, ), migrations.CreateModel( name='User', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('username', models.CharField(max_length=30)), ('date', models.DateTimeField(auto_now=True)), ('session_key', models.CharField(blank=True, max_length=40, null=True)), ], ), migrations.AddField( model_name='serviceticket', name='user', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='serviceticket', to='cas_server.User'), ), migrations.AddField( model_name='proxyticket', name='user', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='proxyticket', to='cas_server.User'), ), migrations.AddField( model_name='proxygrantingticket', name='user', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='proxygrantingticket', to='cas_server.User'), ), migrations.AddField( model_name='proxy', name='proxy_ticket', field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='proxies', to='cas_server.ProxyTicket'), ), migrations.AddField( model_name='proxygrantingticket', name='service_pattern', field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='proxygrantingticket', to='cas_server.ServicePattern'), preserve_default=False, ), migrations.AddField( model_name='proxyticket', name='service_pattern', field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='proxyticket', to='cas_server.ServicePattern'), preserve_default=False, ), migrations.AddField( model_name='serviceticket', name='service_pattern', field=models.ForeignKey(default=1, on_delete=django.db.models.deletion.CASCADE, related_name='serviceticket', to='cas_server.ServicePattern'), preserve_default=False, ), migrations.CreateModel( name='ReplaceAttributName', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('name', models.CharField(help_text="nom d'un attributs \xe0 transmettre au service", max_length=255)), ('replace', models.CharField(blank=True, help_text="nom sous lequel l'attribut sera pr\xe9sent\xe9 au service. vide = inchang\xe9", max_length=255)), ('service_pattern', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='attributs', to='cas_server.ServicePattern')), ], ), migrations.RemoveField( model_name='servicepattern', name='attributs', ), migrations.CreateModel( name='FilterAttributValue', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('attribut', models.CharField(help_text='Name of the attribute which must verify pattern', max_length=255, verbose_name='attribute')), ('pattern', models.CharField(help_text='a regular expression', max_length=255, validators=[cas_server.utils.regexpr_validator], verbose_name='pattern')), ('service_pattern', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='filters', to='cas_server.ServicePattern')), ], ), migrations.CreateModel( name='ReplaceAttributValue', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('attribut', models.CharField(help_text='Name of the attribute for which the value must be replace', max_length=255, verbose_name='attribute')), ('pattern', models.CharField(help_text='An regular expression maching whats need to be replaced', max_length=255, validators=[cas_server.utils.regexpr_validator], verbose_name='pattern')), ('replace', models.CharField(blank=True, help_text='replace expression, groups are capture by \\1, \\2 \u2026', max_length=255, verbose_name='replace')), ('service_pattern', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='replacements', to='cas_server.ServicePattern')), ], ), migrations.RemoveField( model_name='servicepattern', name='filter', ), migrations.CreateModel( name='Username', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('value', models.CharField(help_text='username allowed to connect to the service', max_length=255, verbose_name='username')), ('service_pattern', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='usernames', to='cas_server.ServicePattern')), ], ), migrations.RemoveField( model_name='servicepattern', name='usernames', ), migrations.AddField( model_name='servicepattern', name='restrict_users', field=models.BooleanField(default=False, help_text='Limit username allowed to connect to the list provided bellow', verbose_name='restrict username'), ), migrations.AddField( model_name='servicepattern', name='name', field=models.CharField(blank=True, help_text='A name for the service', max_length=255, null=True, unique=True, verbose_name='name'), ), migrations.AlterUniqueTogether( name='replaceattributname', unique_together=set([('name', 'replace', 'service_pattern')]), ), migrations.AddField( model_name='servicepattern', name='single_log_out', field=models.BooleanField(default=False, help_text='Enable SLO for the service', verbose_name='single log out'), ), migrations.AlterField( model_name='replaceattributname', name='name', field=models.CharField(help_text='name of an attribut to send to the service', max_length=255, verbose_name='name'), ), migrations.AlterField( model_name='replaceattributname', name='replace', field=models.CharField(blank=True, help_text='name under which the attribut will be showto the service. empty = default name of the attribut', max_length=255, verbose_name='replace'), ), migrations.AlterField( model_name='servicepattern', name='pattern', field=models.CharField(max_length=255, unique=True, verbose_name='pattern'), ), migrations.AlterField( model_name='servicepattern', name='pos', field=models.IntegerField(default=100, verbose_name='position'), ), migrations.AlterField( model_name='servicepattern', name='proxy', field=models.BooleanField(default=False, help_text='A ProxyGrantingTicket can be delivered to the service in order to authenticate for the user on a backend service', verbose_name='proxy'), ), migrations.AlterField( model_name='servicepattern', name='user_field', field=models.CharField(blank=True, default=b'', help_text='Name of the attribut to transmit as username, empty = login', max_length=255, verbose_name='user field'), ), migrations.AddField( model_name='proxygrantingticket', name='single_log_out', field=models.BooleanField(default=False), ), migrations.AddField( model_name='proxyticket', name='single_log_out', field=models.BooleanField(default=False), ), migrations.AddField( model_name='serviceticket', name='single_log_out', field=models.BooleanField(default=False), ), migrations.AddField( model_name='servicepattern', name='proxy_callback', field=models.BooleanField(default=False, help_text='can be used as a proxy callback to deliver PGT', verbose_name='proxy callback'), ), migrations.AlterField( model_name='servicepattern', name='proxy', field=models.BooleanField(default=False, help_text='Proxy tickets can be delivered to the service', verbose_name='proxy'), ), migrations.AddField( model_name='servicepattern', name='single_log_out_callback', field=models.CharField(blank=True, default=b'', help_text='URL where the SLO request will be POST. empty = service url\nThis is usefull for non HTTP proxied services.', max_length=255, verbose_name='single log out callback'), ), migrations.AlterField( model_name='replaceattributname', name='name', field=models.CharField(help_text='name of an attribut to send to the service, use * for all attributes', max_length=255, verbose_name='name'), ), migrations.AlterUniqueTogether( name='user', unique_together=set([('username', 'session_key')]), ), migrations.AlterField( model_name='servicepattern', name='pattern', field=models.CharField(help_text="A regular expression matching services. Will usually looks like '^https://some\\.server\\.com/path/.*$'.As it is a regular expression, special character must be escaped with a '\\'.", max_length=255, unique=True, verbose_name='pattern'), ), migrations.AlterModelOptions( name='servicepattern', options={'ordering': ('pos',), 'verbose_name': 'Service pattern', 'verbose_name_plural': 'Services patterns'}, ), migrations.AlterModelOptions( name='user', options={'verbose_name': 'User', 'verbose_name_plural': 'Users'}, ), migrations.AlterField( model_name='servicepattern', name='pos', field=models.IntegerField(default=100, help_text='service patterns are sorted using the position attribute', verbose_name='position'), ), migrations.CreateModel( name='FederatedIendityProvider', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('suffix', models.CharField(help_text='Suffix append to backend CAS returner username: `returned_username`@`suffix`', max_length=30, unique=True, verbose_name='suffix')), ('server_url', models.CharField(max_length=255, verbose_name='server url')), ('cas_protocol_version', models.CharField(choices=[(b'1', b'CAS 1.0'), (b'2', b'CAS 2.0'), (b'3', b'CAS 3.0'), (b'CAS_2_SAML_1_0', b'SAML 1.1')], default=b'3', help_text='Version of the CAS protocol to use when sending requests the the backend CAS', max_length=30, verbose_name='CAS protocol version')), ('verbose_name', models.CharField(help_text='Name for this identity provider displayed on the login page', max_length=255, verbose_name='verbose name')), ('pos', models.IntegerField(default=100, help_text='Identity provider are sorted using the (position, verbose name, suffix) attributes', verbose_name='position')), ('display', models.BooleanField(default=True, help_text='Display the provider on the login page', verbose_name='display')), ], options={ 'verbose_name': 'identity provider', 'verbose_name_plural': 'identity providers', }, ), migrations.CreateModel( name='FederatedUser', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('username', models.CharField(max_length=124)), ('provider', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='cas_server.FederatedIendityProvider')), ('ticket', models.CharField(max_length=255)), ('last_update', models.DateTimeField(auto_now=True)), ('_attributs', models.TextField(blank=True, default=None, null=True)), ], ), migrations.AlterUniqueTogether( name='federateduser', unique_together=set([('username', 'provider')]), ), migrations.CreateModel( name='FederateSLO', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('username', models.CharField(max_length=30)), ('session_key', models.CharField(blank=True, max_length=40, null=True)), ('ticket', models.CharField(db_index=True, max_length=255)), ], ), migrations.AlterUniqueTogether( name='federateslo', unique_together=set([('username', 'session_key', 'ticket')]), ), migrations.AlterField( model_name='federatediendityprovider', name='cas_protocol_version', field=models.CharField(choices=[(b'1', b'CAS 1.0'), (b'2', b'CAS 2.0'), (b'3', b'CAS 3.0'), (b'CAS_2_SAML_1_0', b'SAML 1.1')], default=b'3', help_text='Version of the CAS protocol to use when sending requests the the backend CAS.', max_length=30, verbose_name='CAS protocol version'), ), migrations.AlterField( model_name='federatediendityprovider', name='display', field=models.BooleanField(default=True, help_text='Display the provider on the login page.', verbose_name='display'), ), migrations.AlterField( model_name='federatediendityprovider', name='pos', field=models.IntegerField(default=100, help_text='Position of the identity provider on the login page. Identity provider are sorted using the (position, verbose name, suffix) attributes.', verbose_name='position'), ), migrations.AlterField( model_name='federatediendityprovider', name='suffix', field=models.CharField(help_text='Suffix append to backend CAS returner username: ``returned_username`` @ ``suffix``.', max_length=30, unique=True, verbose_name='suffix'), ), migrations.AlterField( model_name='federatediendityprovider', name='verbose_name', field=models.CharField(help_text='Name for this identity provider displayed on the login page.', max_length=255, verbose_name='verbose name'), ), migrations.RemoveField( model_name='proxygrantingticket', name='attributs', ), migrations.RemoveField( model_name='proxyticket', name='attributs', ), migrations.RemoveField( model_name='serviceticket', name='attributs', ), migrations.AddField( model_name='proxygrantingticket', name='_attributs', field=models.TextField(blank=True, default=None, null=True), ), migrations.AddField( model_name='proxyticket', name='_attributs', field=models.TextField(blank=True, default=None, null=True), ), migrations.AddField( model_name='serviceticket', name='_attributs', field=models.TextField(blank=True, default=None, null=True), ), migrations.AlterField( model_name='federatediendityprovider', name='suffix', field=models.CharField(help_text='Suffix append to backend CAS returned username: ``returned_username`` @ ``suffix``.', max_length=30, unique=True, verbose_name='suffix'), ), migrations.CreateModel( name='NewVersionWarning', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('version', models.CharField(max_length=255)), ], ), migrations.AlterField( model_name='replaceattributname', name='name', field=models.CharField(help_text='name of an attribute to send to the service, use * for all attributes', max_length=255, verbose_name='name'), ), migrations.AlterField( model_name='replaceattributname', name='replace', field=models.CharField(blank=True, help_text='name under which the attribute will be showto the service. empty = default name of the attribut', max_length=255, verbose_name='replace'), ), migrations.AlterField( model_name='servicepattern', name='user_field', field=models.CharField(blank=True, default=b'', help_text='Name of the attribute to transmit as username, empty = login', max_length=255, verbose_name='user field'), ), migrations.AlterField( model_name='replaceattributname', name='replace', field=models.CharField(blank=True, help_text='name under which the attribute will be show to the service. empty = default name of the attribut', max_length=255, verbose_name='replace'), ), migrations.AlterField( model_name='servicepattern', name='pattern', field=models.CharField(help_text="A regular expression matching services. Will usually looks like '^https://some\\.server\\.com/path/.*$'.As it is a regular expression, special character must be escaped with a '\\'.", max_length=255, unique=True, validators=[cas_server.utils.regexpr_validator], verbose_name='pattern'), ), migrations.CreateModel( name='UserAttributes', fields=[ ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), ('_attributs', models.TextField(blank=True, default=None, null=True)), ('username', models.CharField(max_length=155, unique=True)), ], options={ 'verbose_name': 'User attributes cache', 'verbose_name_plural': 'User attributes caches', }, ), migrations.AlterModelOptions( name='federateduser', options={'verbose_name': 'Federated user', 'verbose_name_plural': 'Federated users'}, ), migrations.AddField( model_name='user', name='last_login', field=models.DateTimeField(auto_now_add=True, default=django.utils.timezone.now), preserve_default=False, ), migrations.AlterField( model_name='federatediendityprovider', name='cas_protocol_version', field=models.CharField(choices=[('1', 'CAS 1.0'), ('2', 'CAS 2.0'), ('3', 'CAS 3.0'), ('CAS_2_SAML_1_0', 'SAML 1.1')], default='3', help_text='Version of the CAS protocol to use when sending requests the the backend CAS.', max_length=30, verbose_name='CAS protocol version'), ), migrations.AlterField( model_name='servicepattern', name='single_log_out_callback', field=models.CharField(blank=True, default='', help_text='URL where the SLO request will be POST. empty = service url\nThis is usefull for non HTTP proxied services.', max_length=255, verbose_name='single log out callback'), ), migrations.AlterField( model_name='servicepattern', name='user_field', field=models.CharField(blank=True, default='', help_text='Name of the attribute to transmit as username, empty = login', max_length=255, verbose_name='user field'), ), migrations.AlterField( model_name='user', name='username', field=models.CharField(max_length=250), ), ] django-cas-server-1.1.0/cas_server/models.py0000644000175000017500000012415213416345433022511 0ustar valentinvalentin00000000000000# -*- coding: utf-8 -*- # 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 General Public License version 3 for # more details. # # You should have received a copy of the GNU General Public License version 3 # along with this program; if not, write to the Free Software Foundation, Inc., 51 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # (c) 2015-2016 Valentin Samir """models for the app""" from .default_settings import settings, SessionStore from django.db import models from django.db.models import Q from django.contrib import messages from django.utils.translation import ugettext_lazy as _ from django.utils import timezone from django.utils.encoding import python_2_unicode_compatible from django.core.mail import send_mail import re import sys import smtplib import logging from datetime import timedelta from concurrent.futures import ThreadPoolExecutor from requests_futures.sessions import FuturesSession from cas_server import utils from . import VERSION #: logger facility logger = logging.getLogger(__name__) class JsonAttributes(models.Model): """ Bases: :class:`django.db.models.Model` A base class for models storing attributes as a json """ class Meta: abstract = True #: The attributes json encoded _attributs = models.TextField(default=None, null=True, blank=True) @property def attributs(self): """The attributes""" if self._attributs is not None: return utils.json.loads(self._attributs) @attributs.setter def attributs(self, value): """attributs property setter""" self._attributs = utils.json_encode(value) @python_2_unicode_compatible class FederatedIendityProvider(models.Model): """ Bases: :class:`django.db.models.Model` An identity provider for the federated mode """ class Meta: verbose_name = _(u"identity provider") verbose_name_plural = _(u"identity providers") #: Suffix append to backend CAS returned username: ``returned_username`` @ ``suffix``. #: it must be unique. suffix = models.CharField( max_length=30, unique=True, verbose_name=_(u"suffix"), help_text=_( u"Suffix append to backend CAS returned " u"username: ``returned_username`` @ ``suffix``." ) ) #: URL to the root of the CAS server application. If login page is #: https://cas.example.net/cas/login then :attr:`server_url` should be #: https://cas.example.net/cas/ server_url = models.CharField(max_length=255, verbose_name=_(u"server url")) #: Version of the CAS protocol to use when sending requests the the backend CAS. cas_protocol_version = models.CharField( max_length=30, choices=[ ("1", "CAS 1.0"), ("2", "CAS 2.0"), ("3", "CAS 3.0"), ("CAS_2_SAML_1_0", "SAML 1.1") ], verbose_name=_(u"CAS protocol version"), help_text=_( u"Version of the CAS protocol to use when sending requests the the backend CAS." ), default="3" ) #: Name for this identity provider displayed on the login page. verbose_name = models.CharField( max_length=255, verbose_name=_(u"verbose name"), help_text=_(u"Name for this identity provider displayed on the login page.") ) #: Position of the identity provider on the login page. Identity provider are sorted using the #: (:attr:`pos`, :attr:`verbose_name`, :attr:`suffix`) attributes. pos = models.IntegerField( default=100, verbose_name=_(u"position"), help_text=_( ( u"Position of the identity provider on the login page. " u"Identity provider are sorted using the " u"(position, verbose name, suffix) attributes." ) ) ) #: Display the provider on the login page. Beware that this do not disable the identity #: provider, it just hide it on the login page. User will always be able to log in using this #: provider by fetching ``/federate/suffix``. display = models.BooleanField( default=True, verbose_name=_(u"display"), help_text=_("Display the provider on the login page.") ) def __str__(self): return self.verbose_name @staticmethod def build_username_from_suffix(username, suffix): """ Transform backend username into federated username using ``suffix`` :param unicode username: A CAS backend returned username :param unicode suffix: A suffix identifying the CAS backend :return: The federated username: ``username`` @ ``suffix``. :rtype: unicode """ return u'%s@%s' % (username, suffix) def build_username(self, username): """ Transform backend username into federated username :param unicode username: A CAS backend returned username :return: The federated username: ``username`` @ :attr:`suffix`. :rtype: unicode """ return u'%s@%s' % (username, self.suffix) @python_2_unicode_compatible class FederatedUser(JsonAttributes): """ Bases: :class:`JsonAttributes` A federated user as returner by a CAS provider (username and attributes) """ class Meta: unique_together = ("username", "provider") verbose_name = _("Federated user") verbose_name_plural = _("Federated users") #: The user username returned by the CAS backend on successful ticket validation username = models.CharField(max_length=124) #: A foreign key to :class:`FederatedIendityProvider` provider = models.ForeignKey(FederatedIendityProvider, on_delete=models.CASCADE) #: The last ticket used to authenticate :attr:`username` against :attr:`provider` ticket = models.CharField(max_length=255) #: Last update timespampt. Usually, the last time :attr:`ticket` has been set. last_update = models.DateTimeField(auto_now=True) def __str__(self): return self.federated_username @property def federated_username(self): """The federated username with a suffix for the current :class:`FederatedUser`.""" return self.provider.build_username(self.username) @classmethod def get_from_federated_username(cls, username): """ :return: A :class:`FederatedUser` object from a federated ``username`` :rtype: :class:`FederatedUser` """ if username is None: raise cls.DoesNotExist() else: component = username.split('@') username = '@'.join(component[:-1]) suffix = component[-1] try: provider = FederatedIendityProvider.objects.get(suffix=suffix) return cls.objects.get(username=username, provider=provider) except FederatedIendityProvider.DoesNotExist: raise cls.DoesNotExist() @classmethod def clean_old_entries(cls): """remove old unused :class:`FederatedUser`""" federated_users = cls.objects.filter( last_update__lt=(timezone.now() - timedelta(seconds=settings.CAS_TICKET_TIMEOUT)) ) known_users = {user.username for user in User.objects.all()} for user in federated_users: if user.federated_username not in known_users: user.delete() class FederateSLO(models.Model): """ Bases: :class:`django.db.models.Model` An association between a CAS provider ticket and a (username, session) for processing SLO """ class Meta: unique_together = ("username", "session_key", "ticket") #: the federated username with the ``@``component username = models.CharField(max_length=30) #: the session key for the session :attr:`username` has been authenticated using :attr:`ticket` session_key = models.CharField(max_length=40, blank=True, null=True) #: The ticket used to authenticate :attr:`username` ticket = models.CharField(max_length=255, db_index=True) @classmethod def clean_deleted_sessions(cls): """remove old :class:`FederateSLO` object for which the session do not exists anymore""" for federate_slo in cls.objects.all(): if not SessionStore(session_key=federate_slo.session_key).get('authenticated'): federate_slo.delete() @python_2_unicode_compatible class UserAttributes(JsonAttributes): """ Bases: :class:`JsonAttributes` Local cache of the user attributes, used then needed """ class Meta: verbose_name = _("User attributes cache") verbose_name_plural = _("User attributes caches") #: The username of the user for which we cache attributes username = models.CharField(max_length=155, unique=True) def __str__(self): return self.username @classmethod def clean_old_entries(cls): """Remove :class:`UserAttributes` for which no more :class:`User` exists.""" for user in cls.objects.all(): if User.objects.filter(username=user.username).count() == 0: user.delete() @python_2_unicode_compatible class User(models.Model): """ Bases: :class:`django.db.models.Model` A user logged into the CAS """ class Meta: unique_together = ("username", "session_key") verbose_name = _("User") verbose_name_plural = _("Users") #: The session key of the current authenticated user session_key = models.CharField(max_length=40, blank=True, null=True) #: The username of the current authenticated user username = models.CharField(max_length=250) #: Last time the authenticated user has do something (auth, fetch ticket, etc…) date = models.DateTimeField(auto_now=True) #: last time the user logged last_login = models.DateTimeField(auto_now_add=True) def delete(self, *args, **kwargs): """ Remove the current :class:`User`. If ``settings.CAS_FEDERATE`` is ``True``, also delete the corresponding :class:`FederateSLO` object. """ if settings.CAS_FEDERATE: FederateSLO.objects.filter( username=self.username, session_key=self.session_key ).delete() super(User, self).delete(*args, **kwargs) @classmethod def clean_old_entries(cls): """ Remove :class:`User` objects inactive since more that :django:setting:`SESSION_COOKIE_AGE` and send corresponding SingleLogOut requests. """ filter = Q(date__lt=(timezone.now() - timedelta(seconds=settings.SESSION_COOKIE_AGE))) if settings.CAS_TGT_VALIDITY is not None: filter |= Q( last_login__lt=(timezone.now() - timedelta(seconds=settings.CAS_TGT_VALIDITY)) ) users = cls.objects.filter(filter) for user in users: user.logout() users.delete() @classmethod def clean_deleted_sessions(cls): """Remove :class:`User` objects where the corresponding session do not exists anymore.""" for user in cls.objects.all(): if not SessionStore(session_key=user.session_key).get('authenticated'): user.logout() user.delete() @property def attributs(self): """ Property. A fresh :class:`dict` for the user attributes, using ``settings.CAS_AUTH_CLASS`` if possible, and if not, try to fallback to cached attributes (actually only used for ldap auth class with bind password check mthode). """ try: return utils.import_attr(settings.CAS_AUTH_CLASS)(self.username).attributs() except NotImplementedError: try: user = UserAttributes.objects.get(username=self.username) attributes = user.attributs if attributes is not None: return attributes else: return {} except UserAttributes.DoesNotExist: return {} def __str__(self): return u"%s - %s" % (self.username, self.session_key) def logout(self, request=None): """ Send SLO requests to all services the user is logged in. :param request: The current django HttpRequest to display possible failure to the user. :type request: :class:`django.http.HttpRequest` or :obj:`NoneType` """ ticket_classes = [ProxyGrantingTicket, ServiceTicket, ProxyTicket] for error in Ticket.send_slos( [ticket_class.objects.filter(user=self) for ticket_class in ticket_classes] ): logger.warning( "Error during SLO for user %s: %s" % ( self.username, error ) ) if request is not None: error = utils.unpack_nested_exception(error) messages.add_message( request, messages.WARNING, _(u'Error during service logout %s') % error ) def get_ticket(self, ticket_class, service, service_pattern, renew): """ Generate a ticket using ``ticket_class`` for the service ``service`` matching ``service_pattern`` and asking or not for authentication renewal with ``renew`` :param type ticket_class: :class:`ServiceTicket` or :class:`ProxyTicket` or :class:`ProxyGrantingTicket`. :param unicode service: The service url for which we want a ticket. :param ServicePattern service_pattern: The service pattern matching ``service``. Beware that ``service`` must match :attr:`ServicePattern.pattern` and the current :class:`User` must pass :meth:`ServicePattern.check_user`. These checks are not done here and you must perform them before calling this method. :param bool renew: Should be ``True`` if authentication has been renewed. Must be ``False`` otherwise. :return: A :class:`Ticket` object. :rtype: :class:`ServiceTicket` or :class:`ProxyTicket` or :class:`ProxyGrantingTicket`. """ attributs = dict( (a.name, a.replace if a.replace else a.name) for a in service_pattern.attributs.all() ) replacements = dict( (a.attribut, (a.pattern, a.replace)) for a in service_pattern.replacements.all() ) service_attributs = {} for (key, value) in self.attributs.items(): if key in attributs or '*' in attributs: if key in replacements: if isinstance(value, list): for index, subval in enumerate(value): value[index] = re.sub( replacements[key][0], replacements[key][1], subval ) else: value = re.sub(replacements[key][0], replacements[key][1], value) service_attributs[attributs.get(key, key)] = value ticket = ticket_class.objects.create( user=self, attributs=service_attributs, service=service, renew=renew, service_pattern=service_pattern, single_log_out=service_pattern.single_log_out ) ticket.save() self.save() return ticket def get_service_url(self, service, service_pattern, renew): """ Return the url to which the user must be redirected to after a Service Ticket has been generated :param unicode service: The service url for which we want a ticket. :param ServicePattern service_pattern: The service pattern matching ``service``. Beware that ``service`` must match :attr:`ServicePattern.pattern` and the current :class:`User` must pass :meth:`ServicePattern.check_user`. These checks are not done here and you must perform them before calling this method. :param bool renew: Should be ``True`` if authentication has been renewed. Must be ``False`` otherwise. :return unicode: The service url with the ticket GET param added. :rtype: unicode """ ticket = self.get_ticket(ServiceTicket, service, service_pattern, renew) url = utils.update_url(service, {'ticket': ticket.value}) logger.info("Service ticket created for service %s by user %s." % (service, self.username)) return url class ServicePatternException(Exception): """ Bases: :class:`exceptions.Exception` Base exception of exceptions raised in the ServicePattern model""" pass class BadUsername(ServicePatternException): """ Bases: :class:`ServicePatternException` Exception raised then an non allowed username try to get a ticket for a service """ pass class BadFilter(ServicePatternException): """ Bases: :class:`ServicePatternException` Exception raised then a user try to get a ticket for a service and do not reach a condition """ pass class UserFieldNotDefined(ServicePatternException): """ Bases: :class:`ServicePatternException` Exception raised then a user try to get a ticket for a service using as username an attribut not present on this user """ pass @python_2_unicode_compatible class ServicePattern(models.Model): """ Bases: :class:`django.db.models.Model` Allowed services pattern against services are tested to """ class Meta: ordering = ("pos", ) verbose_name = _("Service pattern") verbose_name_plural = _("Services patterns") #: service patterns are sorted using the :attr:`pos` attribute pos = models.IntegerField( default=100, verbose_name=_(u"position"), help_text=_(u"service patterns are sorted using the position attribute") ) #: A name for the service (this can bedisplayed to the user on the login page) name = models.CharField( max_length=255, unique=True, blank=True, null=True, verbose_name=_(u"name"), help_text=_(u"A name for the service") ) #: A regular expression matching services. "Will usually looks like #: '^https://some\\.server\\.com/path/.*$'. As it is a regular expression, special character #: must be escaped with a '\\'. pattern = models.CharField( max_length=255, unique=True, verbose_name=_(u"pattern"), help_text=_( "A regular expression matching services. " "Will usually looks like '^https://some\\.server\\.com/path/.*$'." "As it is a regular expression, special character must be escaped with a '\\'." ), validators=[utils.regexpr_validator] ) #: Name of the attribute to transmit as username, if empty the user login is used user_field = models.CharField( max_length=255, default="", blank=True, verbose_name=_(u"user field"), help_text=_("Name of the attribute to transmit as username, empty = login") ) #: A boolean allowing to limit username allowed to connect to :attr:`usernames`. restrict_users = models.BooleanField( default=False, verbose_name=_(u"restrict username"), help_text=_("Limit username allowed to connect to the list provided bellow") ) #: A boolean allowing to deliver :class:`ProxyTicket` to the service. proxy = models.BooleanField( default=False, verbose_name=_(u"proxy"), help_text=_("Proxy tickets can be delivered to the service") ) #: A boolean allowing the service to be used as a proxy callback (via the pgtUrl GET param) #: to deliver :class:`ProxyGrantingTicket`. proxy_callback = models.BooleanField( default=False, verbose_name=_(u"proxy callback"), help_text=_("can be used as a proxy callback to deliver PGT") ) #: Enable SingleLogOut for the service. Old validaed tickets for the service will be kept #: until ``settings.CAS_TICKET_TIMEOUT`` after what a SLO request is send to the service and #: the ticket is purged from database. A SLO can be send earlier if the user log-out. single_log_out = models.BooleanField( default=False, verbose_name=_(u"single log out"), help_text=_("Enable SLO for the service") ) #: An URL where the SLO request will be POST. If empty the service url will be used. #: This is usefull for non HTTP proxied services like smtp or imap. single_log_out_callback = models.CharField( max_length=255, default="", blank=True, verbose_name=_(u"single log out callback"), help_text=_(u"URL where the SLO request will be POST. empty = service url\n" u"This is usefull for non HTTP proxied services.") ) def __str__(self): return u"%s: %s" % (self.pos, self.pattern) def check_user(self, user): """ Check if ``user`` if allowed to use theses services. If ``user`` is not allowed, raises one of :class:`BadFilter`, :class:`UserFieldNotDefined`, :class:`BadUsername` :param User user: a :class:`User` object :raises BadUsername: if :attr:`restrict_users` if ``True`` and :attr:`User.username` is not within :attr:`usernames`. :raises BadFilter: if a :class:`FilterAttributValue` condition of :attr:`filters` connot be verified. :raises UserFieldNotDefined: if :attr:`user_field` is defined and its value is not within :attr:`User.attributs`. :return: ``True`` :rtype: bool """ if self.restrict_users and not self.usernames.filter(value=user.username): logger.warning("Username %s not allowed on service %s" % (user.username, self.name)) raise BadUsername() for filtre in self.filters.all(): if isinstance(user.attributs.get(filtre.attribut, []), list): attrs = user.attributs.get(filtre.attribut, []) else: attrs = [user.attributs[filtre.attribut]] for value in attrs: if re.match(filtre.pattern, str(value)): break else: bad_filter = (filtre.pattern, filtre.attribut, user.attributs.get(filtre.attribut)) logger.warning( "User constraint failed for %s, service %s: %s do not match %s %s." % ( (user.username, self.name) + bad_filter ) ) raise BadFilter('%s do not match %s %s' % bad_filter) if self.user_field and not user.attributs.get(self.user_field): logger.warning( "Cannot use %s a loggin for user %s on service %s because it is absent" % ( self.user_field, user.username, self.name ) ) raise UserFieldNotDefined() return True @classmethod def validate(cls, service): """ Get a :class:`ServicePattern` intance from a service url. :param unicode service: A service url :return: A :class:`ServicePattern` instance matching ``service``. :rtype: :class:`ServicePattern` :raises ServicePattern.DoesNotExist: if no :class:`ServicePattern` is matching ``service``. """ for service_pattern in cls.objects.all().order_by('pos'): if re.match(service_pattern.pattern, service): return service_pattern logger.warning("Service %s not allowed." % service) raise cls.DoesNotExist() @python_2_unicode_compatible class Username(models.Model): """ Bases: :class:`django.db.models.Model` A list of allowed usernames on a :class:`ServicePattern` """ #: username allowed to connect to the service value = models.CharField( max_length=255, verbose_name=_(u"username"), help_text=_(u"username allowed to connect to the service") ) #: ForeignKey to a :class:`ServicePattern`. :class:`Username` instances for a #: :class:`ServicePattern` are accessible thought its :attr:`ServicePattern.usernames` #: attribute. service_pattern = models.ForeignKey( ServicePattern, related_name="usernames", on_delete=models.CASCADE ) def __str__(self): return self.value @python_2_unicode_compatible class ReplaceAttributName(models.Model): """ Bases: :class:`django.db.models.Model` A replacement of an attribute name for a :class:`ServicePattern`. It also tell to transmit an attribute of :attr:`User.attributs` to the service. An empty :attr:`replace` mean to use the original attribute name. """ class Meta: unique_together = ('name', 'replace', 'service_pattern') #: Name the attribute: a key of :attr:`User.attributs` name = models.CharField( max_length=255, verbose_name=_(u"name"), help_text=_(u"name of an attribute to send to the service, use * for all attributes") ) #: The name of the attribute to transmit to the service. If empty, the value of :attr:`name` #: is used. replace = models.CharField( max_length=255, blank=True, verbose_name=_(u"replace"), help_text=_(u"name under which the attribute will be show " u"to the service. empty = default name of the attribut") ) #: ForeignKey to a :class:`ServicePattern`. :class:`ReplaceAttributName` instances for a #: :class:`ServicePattern` are accessible thought its :attr:`ServicePattern.attributs` #: attribute. service_pattern = models.ForeignKey( ServicePattern, related_name="attributs", on_delete=models.CASCADE ) def __str__(self): if not self.replace: return self.name else: return u"%s → %s" % (self.name, self.replace) @python_2_unicode_compatible class FilterAttributValue(models.Model): """ Bases: :class:`django.db.models.Model` A filter on :attr:`User.attributs` for a :class:`ServicePattern`. If a :class:`User` do not have an attribute :attr:`attribut` or its value do not match :attr:`pattern`, then :meth:`ServicePattern.check_user` will raises :class:`BadFilter` if called with that user. """ #: The name of a user attribute attribut = models.CharField( max_length=255, verbose_name=_(u"attribute"), help_text=_(u"Name of the attribute which must verify pattern") ) #: A regular expression the attribute :attr:`attribut` value must verify. If :attr:`attribut` #: if a list, only one of the list values needs to match. pattern = models.CharField( max_length=255, verbose_name=_(u"pattern"), help_text=_(u"a regular expression"), validators=[utils.regexpr_validator] ) #: ForeignKey to a :class:`ServicePattern`. :class:`FilterAttributValue` instances for a #: :class:`ServicePattern` are accessible thought its :attr:`ServicePattern.filters` #: attribute. service_pattern = models.ForeignKey( ServicePattern, related_name="filters", on_delete=models.CASCADE ) def __str__(self): return u"%s %s" % (self.attribut, self.pattern) @python_2_unicode_compatible class ReplaceAttributValue(models.Model): """ Bases: :class:`django.db.models.Model` A replacement (using a regular expression) of an attribute value for a :class:`ServicePattern`. """ #: Name the attribute: a key of :attr:`User.attributs` attribut = models.CharField( max_length=255, verbose_name=_(u"attribute"), help_text=_(u"Name of the attribute for which the value must be replace") ) #: A regular expression matching the part of the attribute value that need to be changed pattern = models.CharField( max_length=255, verbose_name=_(u"pattern"), help_text=_(u"An regular expression maching whats need to be replaced"), validators=[utils.regexpr_validator] ) #: The replacement to what is mached by :attr:`pattern`. groups are capture by \\1, \\2 … replace = models.CharField( max_length=255, blank=True, verbose_name=_(u"replace"), help_text=_(u"replace expression, groups are capture by \\1, \\2 …") ) #: ForeignKey to a :class:`ServicePattern`. :class:`ReplaceAttributValue` instances for a #: :class:`ServicePattern` are accessible thought its :attr:`ServicePattern.replacements` #: attribute. service_pattern = models.ForeignKey( ServicePattern, related_name="replacements", on_delete=models.CASCADE ) def __str__(self): return u"%s %s %s" % (self.attribut, self.pattern, self.replace) @python_2_unicode_compatible class Ticket(JsonAttributes): """ Bases: :class:`JsonAttributes` Generic class for a Ticket """ class Meta: abstract = True #: ForeignKey to a :class:`User`. user = models.ForeignKey(User, related_name="%(class)s", on_delete=models.CASCADE) #: A boolean. ``True`` if the ticket has been validated validate = models.BooleanField(default=False) #: The service url for the ticket service = models.TextField() #: ForeignKey to a :class:`ServicePattern`. The :class:`ServicePattern` corresponding to #: :attr:`service`. Use :meth:`ServicePattern.validate` to find it. service_pattern = models.ForeignKey( ServicePattern, related_name="%(class)s", on_delete=models.CASCADE ) #: Date of the ticket creation creation = models.DateTimeField(auto_now_add=True) #: A boolean. ``True`` if the user has just renew his authentication renew = models.BooleanField(default=False) #: A boolean. Set to :attr:`service_pattern` attribute #: :attr:`ServicePattern.single_log_out` value. single_log_out = models.BooleanField(default=False) #: Max duration between ticket creation and its validation. Any validation attempt for the #: ticket after :attr:`creation` + VALIDITY will fail as if the ticket do not exists. VALIDITY = settings.CAS_TICKET_VALIDITY #: Time we keep ticket with :attr:`single_log_out` set to ``True`` before sending SingleLogOut #: requests. TIMEOUT = settings.CAS_TICKET_TIMEOUT class DoesNotExist(Exception): """raised in :meth:`Ticket.get` then ticket prefix and ticket classes mismatch""" pass def __str__(self): return u"Ticket-%s" % self.pk @staticmethod def send_slos(queryset_list): """ Send SLO requests to each ticket of each queryset of ``queryset_list`` :param list queryset_list: A list a :class:`Ticket` queryset :return: A list of possibly encoutered :class:`Exception` :rtype: list """ # sending SLO to timed-out validated tickets async_list = [] session = FuturesSession( executor=ThreadPoolExecutor(max_workers=settings.CAS_SLO_MAX_PARALLEL_REQUESTS) ) errors = [] for queryset in queryset_list: for ticket in queryset: ticket.logout(session, async_list) queryset.delete() for future in async_list: if future: # pragma: no branch (should always be true) try: future.result() except Exception as error: errors.append(error) return errors @classmethod def clean_old_entries(cls): """Remove old ticket and send SLO to timed-out services""" # removing old validated ticket and non validated expired tickets cls.objects.filter( ( Q(single_log_out=False) & Q(validate=True) ) | ( Q(validate=False) & Q(creation__lt=(timezone.now() - timedelta(seconds=cls.VALIDITY))) ) ).delete() queryset = cls.objects.filter( creation__lt=(timezone.now() - timedelta(seconds=cls.TIMEOUT)) ) for error in cls.send_slos([queryset]): logger.warning("Error durring SLO %s" % error) sys.stderr.write("%r\n" % error) def logout(self, session, async_list=None): """Send a SLO request to the ticket service""" # On logout invalidate the Ticket self.validate = True self.save() if self.validate and self.single_log_out: # pragma: no branch (should always be true) logger.info( "Sending SLO requests to service %s for user %s" % ( self.service, self.user.username ) ) xml = utils.logout_request(self.value) if self.service_pattern.single_log_out_callback: url = self.service_pattern.single_log_out_callback else: url = self.service async_list.append( session.post( url.encode('utf-8'), data={'logoutRequest': xml.encode('utf-8')}, timeout=settings.CAS_SLO_TIMEOUT ) ) @staticmethod def get_class(ticket, classes=None): """ Return the ticket class of ``ticket`` :param unicode ticket: A ticket :param list classes: Optinal arguement. A list of possible :class:`Ticket` subclasses :return: The class corresponding to ``ticket`` (:class:`ServiceTicket` or :class:`ProxyTicket` or :class:`ProxyGrantingTicket`) if found among ``classes, ``None`` otherwise. :rtype: :obj:`type` or :obj:`NoneType` """ if classes is None: # pragma: no cover (not used) classes = [ServiceTicket, ProxyTicket, ProxyGrantingTicket] for ticket_class in classes: if ticket.startswith(ticket_class.PREFIX): return ticket_class def username(self): """ The username to send on ticket validation :return: The value of the corresponding user attribute if :attr:`service_pattern`.user_field is set, the user username otherwise. """ if self.service_pattern.user_field and self.user.attributs.get( self.service_pattern.user_field ): username = self.user.attributs[self.service_pattern.user_field] if isinstance(username, list): # the list is not empty because we wont generate a ticket with a user_field # that evaluate to False username = username[0] else: username = self.user.username return username def attributs_flat(self): """ generate attributes list for template rendering :return: An list of (attribute name, attribute value) of all user attributes flatened (no nested list) :rtype: :obj:`list` of :obj:`tuple` of :obj:`unicode` """ attributes = [] for key, value in self.attributs.items(): if isinstance(value, list): for elt in value: attributes.append((key, elt)) else: attributes.append((key, value)) return attributes @classmethod def get(cls, ticket, renew=False, service=None): """ Search the database for a valid ticket with provided arguments :param unicode ticket: A ticket value :param bool renew: Is authentication renewal needed :param unicode service: Optional argument. The ticket service :raises Ticket.DoesNotExist: if no class is found for the ticket prefix :raises cls.DoesNotExist: if ``ticket`` value is not found in th database :return: a :class:`Ticket` instance :rtype: Ticket """ # If the method class is the ticket abstract class, search for the submited ticket # class using its prefix. Assuming ticket is a ProxyTicket or a ServiceTicket if cls == Ticket: ticket_class = cls.get_class(ticket, classes=[ServiceTicket, ProxyTicket]) # else use the method class else: ticket_class = cls # If ticket prefix is wrong, raise DoesNotExist if cls != Ticket and not ticket.startswith(cls.PREFIX): raise Ticket.DoesNotExist() if ticket_class: # search for the ticket that is not yet validated and is still valid ticket_queryset = ticket_class.objects.filter( value=ticket, validate=False, creation__gt=(timezone.now() - timedelta(seconds=ticket_class.VALIDITY)) ) # if service is specified, add it the the queryset if service is not None: ticket_queryset = ticket_queryset.filter(service=service) # only require renew if renew is True, otherwise it do not matter if renew is True # or False. if renew: ticket_queryset = ticket_queryset.filter(renew=True) # fetch the ticket ``MultipleObjectsReturned`` is never raised as the ticket value # is unique across the database ticket = ticket_queryset.get() # For ServiceTicket and Proxyticket, mark it as validated before returning if ticket_class != ProxyGrantingTicket: ticket.validate = True ticket.save() return ticket # If no class found for the ticket, raise DoesNotExist else: raise Ticket.DoesNotExist() @python_2_unicode_compatible class ServiceTicket(Ticket): """ Bases: :class:`Ticket` A Service Ticket """ #: The ticket prefix used to differentiate it from other tickets types PREFIX = settings.CAS_SERVICE_TICKET_PREFIX #: The ticket value value = models.CharField(max_length=255, default=utils.gen_st, unique=True) def __str__(self): return u"ServiceTicket-%s" % self.pk @python_2_unicode_compatible class ProxyTicket(Ticket): """ Bases: :class:`Ticket` A Proxy Ticket """ #: The ticket prefix used to differentiate it from other tickets types PREFIX = settings.CAS_PROXY_TICKET_PREFIX #: The ticket value value = models.CharField(max_length=255, default=utils.gen_pt, unique=True) def __str__(self): return u"ProxyTicket-%s" % self.pk @python_2_unicode_compatible class ProxyGrantingTicket(Ticket): """ Bases: :class:`Ticket` A Proxy Granting Ticket """ #: The ticket prefix used to differentiate it from other tickets types PREFIX = settings.CAS_PROXY_GRANTING_TICKET_PREFIX #: ProxyGranting ticket are never validated. However, they can be used during :attr:`VALIDITY` #: to get :class:`ProxyTicket` for :attr:`user` VALIDITY = settings.CAS_PGT_VALIDITY #: The ticket value value = models.CharField(max_length=255, default=utils.gen_pgt, unique=True) def __str__(self): return u"ProxyGrantingTicket-%s" % self.pk @python_2_unicode_compatible class Proxy(models.Model): """ Bases: :class:`django.db.models.Model` A list of proxies on :class:`ProxyTicket` """ class Meta: ordering = ("-pk", ) #: Service url of the PGT used for getting the associated :class:`ProxyTicket` url = models.CharField(max_length=255) #: ForeignKey to a :class:`ProxyTicket`. :class:`Proxy` instances for a #: :class:`ProxyTicket` are accessible thought its :attr:`ProxyTicket.proxies` #: attribute. proxy_ticket = models.ForeignKey(ProxyTicket, related_name="proxies", on_delete=models.CASCADE) def __str__(self): return self.url class NewVersionWarning(models.Model): """ Bases: :class:`django.db.models.Model` The last new version available version sent """ version = models.CharField(max_length=255) @classmethod def send_mails(cls): """ For each new django-cas-server version, if the current instance is not up to date send one mail to ``settings.ADMINS``. """ if settings.CAS_NEW_VERSION_EMAIL_WARNING and settings.ADMINS: try: obj = cls.objects.get() except cls.DoesNotExist: obj = NewVersionWarning.objects.create(version=VERSION) LAST_VERSION = utils.last_version() if LAST_VERSION is not None and LAST_VERSION != obj.version: if utils.decode_version(VERSION) < utils.decode_version(LAST_VERSION): try: send_mail( ( '%sA new version of django-cas-server is available' ) % settings.EMAIL_SUBJECT_PREFIX, u''' A new version of the django-cas-server is available. Your version: %s New version: %s Upgrade using: * pip install -U django-cas-server * fetching the last release on https://github.com/nitmir/django-cas-server/ or on https://pypi.org/project/django-cas-server/ After upgrade, do not forget to run: * ./manage.py migrate * ./manage.py collectstatic and to reload your wsgi server (apache2, uwsgi, gunicord, etc…) --\u0020 django-cas-server '''.strip() % (VERSION, LAST_VERSION), settings.SERVER_EMAIL, ["%s <%s>" % admin for admin in settings.ADMINS], fail_silently=False, ) obj.version = LAST_VERSION obj.save() except smtplib.SMTPException as error: # pragma: no cover (should not happen) logger.error("Unable to send new version mail: %s" % error) django-cas-server-1.1.0/cas_server/admin.py0000644000175000017500000001523513060003340022276 0ustar valentinvalentin00000000000000# 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 General Public License version 3 for # more details. # # You should have received a copy of the GNU General Public License version 3 # along with this program; if not, write to the Free Software Foundation, Inc., 51 # Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. # # (c) 2015-2016 Valentin Samir """module for the admin interface of the app""" from .default_settings import settings from django.contrib import admin from .models import ServiceTicket, ProxyTicket, ProxyGrantingTicket, User, ServicePattern from .models import Username, ReplaceAttributName, ReplaceAttributValue, FilterAttributValue from .models import FederatedIendityProvider, FederatedUser, UserAttributes from .forms import TicketForm class BaseInlines(admin.TabularInline): """ Bases: :class:`django.contrib.admin.TabularInline` Base class for inlines in the admin interface. """ #: This controls the number of extra forms the formset will display in addition to #: the initial forms. extra = 0 class UserAdminInlines(BaseInlines): """ Bases: :class:`BaseInlines` Base class for inlines in :class:`UserAdmin` interface """ #: The form :class:`TicketForm` used to display tickets. form = TicketForm #: Fields to display on a object that are read only (not editable). readonly_fields = ( 'validate', 'service', 'service_pattern', 'creation', 'renew', 'single_log_out', 'value' ) #: Fields to display on a object. fields = ( 'validate', 'service', 'service_pattern', 'creation', 'renew', 'single_log_out' ) class ServiceTicketInline(UserAdminInlines): """ Bases: :class:`UserAdminInlines` :class:`ServiceTicket` in admin interface """ #: The model which the inline is using. model = ServiceTicket class ProxyTicketInline(UserAdminInlines): """ Bases: :class:`UserAdminInlines` :class:`ProxyTicket` in admin interface """ #: The model which the inline is using. model = ProxyTicket class ProxyGrantingInline(UserAdminInlines): """ Bases: :class:`UserAdminInlines` :class:`ProxyGrantingTicket` in admin interface """ #: The model which the inline is using. model = ProxyGrantingTicket class UserAdmin(admin.ModelAdmin): """ Bases: :class:`django.contrib.admin.ModelAdmin` :class:`User` in admin interface """ #: See :class:`ServiceTicketInline`, :class:`ProxyTicketInline`, :class:`ProxyGrantingInline` #: objects below the :class:`UserAdmin` fields. inlines = (ServiceTicketInline, ProxyTicketInline, ProxyGrantingInline) #: Fields to display on a object that are read only (not editable). readonly_fields = ('username', 'date', "session_key") #: Fields to display on a object. fields = ('username', 'date', "session_key") #: Fields to display on the list of class:`UserAdmin` objects. list_display = ('username', 'date', "session_key") class UsernamesInline(BaseInlines): """ Bases: :class:`BaseInlines` :class:`Username` in admin interface """ #: The model which the inline is using. model = Username class ReplaceAttributNameInline(BaseInlines): """ Bases: :class:`BaseInlines` :class:`ReplaceAttributName` in admin interface """ #: The model which the inline is using. model = ReplaceAttributName class ReplaceAttributValueInline(BaseInlines): """ Bases: :class:`BaseInlines` :class:`ReplaceAttributValue` in admin interface """ #: The model which the inline is using. model = ReplaceAttributValue class FilterAttributValueInline(BaseInlines): """ Bases: :class:`BaseInlines` :class:`FilterAttributValue` in admin interface """ #: The model which the inline is using. model = FilterAttributValue class ServicePatternAdmin(admin.ModelAdmin): """ Bases: :class:`django.contrib.admin.ModelAdmin` :class:`ServicePattern` in admin interface """ #: See :class:`UsernamesInline`, :class:`ReplaceAttributNameInline`, #: :class:`ReplaceAttributValueInline`, :class:`FilterAttributValueInline` objects below #: the :class:`ServicePatternAdmin` fields. inlines = ( UsernamesInline, ReplaceAttributNameInline, ReplaceAttributValueInline, FilterAttributValueInline ) #: Fields to display on the list of class:`ServicePatternAdmin` objects. list_display = ('pos', 'name', 'pattern', 'proxy', 'single_log_out', 'proxy_callback', 'restrict_users') class FederatedIendityProviderAdmin(admin.ModelAdmin): """ Bases: :class:`django.contrib.admin.ModelAdmin` :class:`FederatedIendityProvider` in admin interface """ #: Fields to display on a object. fields = ('pos', 'suffix', 'server_url', 'cas_protocol_version', 'verbose_name', 'display') #: Fields to display on the list of class:`FederatedIendityProviderAdmin` objects. list_display = ('verbose_name', 'suffix', 'display') class FederatedUserAdmin(admin.ModelAdmin): """ Bases: :class:`django.contrib.admin.ModelAdmin` :class:`FederatedUser` in admin interface """ #: Fields to display on a object. fields = ('username', 'provider', 'last_update') #: Fields to display on the list of class:`FederatedUserAdmin` objects. list_display = ('username', 'provider', 'last_update') class UserAttributesAdmin(admin.ModelAdmin): """ Bases: :class:`django.contrib.admin.ModelAdmin` :class:`UserAttributes` in admin interface """ #: Fields to display on a object. fields = ('username', '_attributs') admin.site.register(ServicePattern, ServicePatternAdmin) admin.site.register(FederatedIendityProvider, FederatedIendityProviderAdmin) if settings.DEBUG: # pragma: no branch (we always test with DEBUG True) admin.site.register(User, UserAdmin) admin.site.register(FederatedUser, FederatedUserAdmin) admin.site.register(UserAttributes, UserAttributesAdmin) django-cas-server-1.1.0/PKG-INFO0000644000175000017500000011400413436457571017621 0ustar valentinvalentin00000000000000Metadata-Version: 1.1 Name: django-cas-server Version: 1.1.0 Summary: A Django Central Authentication Service server implementing the CAS Protocol 3.0 Specification Home-page: https://github.com/nitmir/django-cas-server Author: Valentin Samir Author-email: valentin.samir@crans.org License: GPLv3 Download-URL: https://github.com/nitmir/django-cas-server/releases/latest Description: CAS Server ########## |travis| |coverage| |licence| |github_version| |pypi_version| |codacy| |doc| CAS Server is a Django application implementing the `CAS Protocol 3.0 Specification `_. By default, the authentication process use django internal users but you can easily use any sources (see the `Authentication backend`_ section and auth classes in the auth.py file) .. contents:: Table of Contents Features ======== * Support CAS version 1.0, 2.0, 3.0 * Support Single Sign Out * Configuration of services via the django Admin application * Fine control on which user's attributes are passed to which service * Possibility to rename/rewrite attributes per service * Possibility to require some attribute values per service * Federated mode between multiple CAS * Supports Django 1.11 and 2.0 * Supports Python 2.7, 3.5+ Dependencies ============ ``django-cas-server`` depends on the following python packages: * Django >= 1.11 < 2.1 * requests >= 2.4 * requests_futures >= 0.9.5 * lxml >= 3.4 * six >= 1.8 Minimal version of packages dependancy are just indicative and meens that ``django-cas-server`` has been tested with it. Previous versions of dependencies may or may not work. Additionally, denpending of the `Authentication backend`_ you plan to use, you may need the following python packages: * ldap3 * psycopg2 * mysql-python Here there is a table with the name of python packages and the corresponding packages providing them on debian like systems and centos like systems. You should try as much as possible to use system packages as there are automatically updated then you update your system. You can then install Not Available (N/A) packages on your system using pip inside a virtualenv as described in the `Installation`_ section. For use with python3, just replace python(2) in the table by python3. +------------------+-------------------------+---------------------+ | python package | debian like systems | centos like systems | +==================+=========================+=====================+ | Django | python-django | python-django | +------------------+-------------------------+---------------------+ | requests | python-requests | python-requests | +------------------+-------------------------+---------------------+ | requests_futures | python-requests-futures | N/A | +------------------+-------------------------+---------------------+ | lxml | python-lxml | python-lxml | +------------------+-------------------------+---------------------+ | six | python-six | python-six | +------------------+-------------------------+---------------------+ | ldap3 | python-ldap3 | python-ldap3 | +------------------+-------------------------+---------------------+ | psycopg2 | python-psycopg2 | python-psycopg2 | +------------------+-------------------------+---------------------+ | mysql-python | python-mysqldb | python2-mysql | +------------------+-------------------------+---------------------+ Installation ============ The recommended installation mode is to use a virtualenv with ``--system-site-packages`` 1. Make sure that python virtualenv is installed 2. Install python packages available via the system package manager: On debian like systems:: $ sudo apt-get install python-django python-requests python-six python-lxml python-requests-futures On debian jessie, you can use the version of python-django available in the `backports `_. On centos like systems:: $ sudo yum install python-django python-requests python-six python-lxml 3. Create a virtualenv:: $ virtualenv --system-site-packages cas_venv Running virtualenv with interpreter /var/www/html/cas-server/bin/python2 Using real prefix '/usr' New python executable in cas/bin/python2 Also creating executable in cas/bin/python Installing setuptools, pip...done. 4. And `activate it `__:: $ cd cas_venv/; . bin/activate 5. Create a django project:: $ django-admin startproject cas_project $ cd cas_project 6. Install `django-cas-server`. To use the last published release, run:: $ pip install django-cas-server Alternatively if you want to use the version of the git repository, you can clone it:: $ git clone https://github.com/nitmir/django-cas-server $ cd django-cas-server $ pip install -r requirements.txt Then, either run ``make install`` to create a python package using the sources of the repository and install it with pip, or place the ``cas_server`` directory into your `PYTHONPATH `_ (for instance by symlinking ``cas_server`` to the root of your django project). 7. Open ``cas_project/settings.py`` in you favourite editor and follow the quick start section. Quick start =========== 1. Add "cas_server" to your INSTALLED_APPS setting like this:: INSTALLED_APPS = ( 'django.contrib.admin', ... 'cas_server', ) For internationalization support, add "django.middleware.locale.LocaleMiddleware" to your MIDDLEWARE_CLASSES setting like this:: MIDDLEWARE_CLASSES = ( ... 'django.middleware.locale.LocaleMiddleware', ... ) 2. Include the cas_server URLconf in your project urls.py like this:: from django.conf.urls import url, include urlpatterns = [ url(r'^admin/', admin.site.urls), ... url(r'^cas/', include('cas_server.urls', namespace="cas_server")), ] 3. Run ``python manage.py migrate`` to create the cas_server models. 4. You should add some management commands to a crontab: ``clearsessions``, ``cas_clean_tickets`` and ``cas_clean_sessions``. * ``clearsessions``: please see `Clearing the session store `_. * ``cas_clean_tickets``: old tickets and timed-out tickets do not get purge from the database automatically. They are just marked as invalid. ``cas_clean_tickets`` is a clean-up management command for this purpose. It send SingleLogOut request to services with timed out tickets and delete them. * ``cas_clean_sessions``: Logout and purge users (sending SLO requests) that are inactive since more than ``SESSION_COOKIE_AGE``. The default value for is ``1209600`` seconds (2 weeks). You probably should reduce it to something like ``86400`` seconds (1 day). You could for example do as bellow:: 0 0 * * * cas-user /path/to/project/manage.py clearsessions */5 * * * * cas-user /path/to/project/manage.py cas_clean_tickets 5 0 * * * cas-user /path/to/project/manage.py cas_clean_sessions 5. Run ``python manage.py createsuperuser`` to create an administrator user. 6. Start the development server and visit http://127.0.0.1:8000/admin/ to add a first service allowed to authenticate user against the CAS (you'll need the Admin app enabled). See the `Service Patterns`_ section bellow. 7. Visit http://127.0.0.1:8000/cas/ to login with your django users. Settings ======== All settings are optional. Add them to ``settings.py`` to customize ``django-cas-server``: Template settings ----------------- * ``CAS_LOGO_URL``: URL to the logo showed in the up left corner on the default templates. Set it to ``False`` to disable it. * ``CAS_FAVICON_URL``: URL to the favicon (shortcut icon) used by the default templates. Default is a key icon. Set it to ``False`` to disable it. * ``CAS_SHOW_POWERED``: Set it to ``False`` to hide the powered by footer. The default is ``True``. * ``CAS_COMPONENT_URLS``: URLs to css and javascript external components. It is a dictionnary having the five following keys: ``"bootstrap3_css"``, ``"bootstrap3_js"``, ``"html5shiv"``, ``"respond"``, ``"jquery"``. The default is:: { "bootstrap3_css": "//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css", "bootstrap3_js": "//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js", "html5shiv": "//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js", "respond": "//oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js", "jquery": "//code.jquery.com/jquery.min.js", } if you omit some keys of the dictionnary, the default value for these keys is used. * ``CAS_SHOW_SERVICE_MESSAGES``: Messages displayed about the state of the service on the login page. The default is ``True``. * ``CAS_INFO_MESSAGES``: Messages displayed in info-boxes on the html pages of the default templates. It is a dictionnary mapping message name to a message dict. A message dict has 3 keys: * ``message``: A unicode message to display, potentially wrapped around ugettex_lazy * ``discardable``: A boolean, specify if the users can close the message info-box * ``type``: One of info, success, info, warning, danger. The type of the info-box. ``CAS_INFO_MESSAGES`` contains by default one message, ``cas_explained``, which explain roughly the purpose of a CAS. The default is:: { "cas_explained": { "message":_( u"The Central Authentication Service grants you access to most of our websites by " u"authenticating only once, so you don't need to type your credentials again unless " u"your session expires or you logout." ), "discardable": True, "type": "info", # one of info, success, info, warning, danger }, } * ``CAS_INFO_MESSAGES_ORDER``: A list of message names. Order in which info-box messages are displayed. Use an empty list to disable messages display. The default is ``[]``. * ``CAS_LOGIN_TEMPLATE``: Path to the template showed on ``/login`` then the user is not autenticated. The default is ``"cas_server/login.html"``. * ``CAS_WARN_TEMPLATE``: Path to the template showed on ``/login?service=...`` then the user is authenticated and has asked to be warned before being connected to a service. The default is ``"cas_server/warn.html"``. * ``CAS_LOGGED_TEMPLATE``: Path to the template showed on ``/login`` then to user is authenticated. The default is ``"cas_server/logged.html"``. * ``CAS_LOGOUT_TEMPLATE``: Path to the template showed on ``/logout`` then to user is being disconnected. The default is ``"cas_server/logout.html"`` * ``CAS_REDIRECT_TO_LOGIN_AFTER_LOGOUT``: Should we redirect users to ``/login`` after they logged out instead of displaying ``CAS_LOGOUT_TEMPLATE``. The default is ``False``. Authentication settings ----------------------- * ``CAS_AUTH_CLASS``: A dotted path to a class or a class implementing ``cas_server.auth.AuthUser``. The default is ``"cas_server.auth.DjangoAuthUser"`` Available classes bundled with ``django-cas-server`` are listed below in the `Authentication backend`_ section. * ``SESSION_COOKIE_AGE``: This is a django settings. Here, it control the delay in seconds after which inactive users are logged out. The default is ``1209600`` (2 weeks). You probably should reduce it to something like ``86400`` seconds (1 day). * ``CAS_TGT_VALIDITY``: Max time after with the user MUST reauthenticate. Let it to `None` for no max time.This can be used to force refreshing cached informations only available upon user authentication like the user attributes in federation mode or with the ldap auth in bind mode. The default is ``None``. * ``CAS_PROXY_CA_CERTIFICATE_PATH``: Path to certificate authorities file. Usually on linux the local CAs are in ``/etc/ssl/certs/ca-certificates.crt``. The default is ``True`` which tell requests to use its internal certificat authorities. Settings it to ``False`` should disable all x509 certificates validation and MUST not be done in production. x509 certificate validation is perform upon PGT issuance. * ``CAS_SLO_MAX_PARALLEL_REQUESTS``: Maximum number of parallel single log out requests send. If more requests need to be send, there are queued. The default is ``10``. * ``CAS_SLO_TIMEOUT``: Timeout for a single SLO request in seconds. The default is ``5``. Federation settings ------------------- * ``CAS_FEDERATE``: A boolean for activating the federated mode (see the `Federation mode`_ section below). The default is ``False``. * ``CAS_FEDERATE_REMEMBER_TIMEOUT``: Time after witch the cookie use for "remember my identity provider" expire. The default is ``604800``, one week. The cookie is called ``_remember_provider``. New version warnings settings ----------------------------- * ``CAS_NEW_VERSION_HTML_WARNING``: A boolean for diplaying a warning on html pages then a new version of the application is avaible. Once closed by a user, it is not displayed to this user until the next new version. The default is ``True``. * ``CAS_NEW_VERSION_EMAIL_WARNING``: A boolean for sending a email to ``settings.ADMINS`` when a new version is available. The default is ``True``. Tickets validity settings ------------------------- * ``CAS_TICKET_VALIDITY``: Number of seconds the service tickets and proxy tickets are valid. This is the maximal time between ticket issuance by the CAS and ticket validation by an application. The default is ``60``. * ``CAS_PGT_VALIDITY``: Number of seconds the proxy granting tickets are valid. The default is ``3600`` (1 hour). * ``CAS_TICKET_TIMEOUT``: Number of seconds a ticket is kept in the database before sending Single Log Out request and being cleared. The default is ``86400`` (24 hours). Tickets miscellaneous settings ------------------------------ * ``CAS_TICKET_LEN``: Default ticket length. All CAS implementation MUST support ST and PT up to 32 chars, PGT and PGTIOU up to 64 chars and it is RECOMMENDED that all tickets up to 256 chars are supports. Here the default is ``64``. * ``CAS_LT_LEN``: Length of the login tickets. Login tickets are only processed by ``django-cas-server`` thus there is no length restriction on it. The default is ``CAS_TICKET_LEN``. * ``CAS_ST_LEN``: Length of the service tickets. The default is ``CAS_TICKET_LEN``. You may need to lower is to ``32`` if you use some old clients. * ``CAS_PT_LEN``: Length of the proxy tickets. The default is ``CAS_TICKET_LEN``. This length should be the same as ``CAS_ST_LEN``. You may need to lower is to ``32`` if you use some old clients. * ``CAS_PGT_LEN``: Length of the proxy granting tickets. The default is ``CAS_TICKET_LEN``. * ``CAS_PGTIOU_LEN``: Length of the proxy granting tickets IOU. The default is ``CAS_TICKET_LEN``. * ``CAS_LOGIN_TICKET_PREFIX``: Prefix of login tickets. The default is ``"LT"``. * ``CAS_SERVICE_TICKET_PREFIX``: Prefix of service tickets. The default is ``"ST"``. The CAS specification mandate that service tickets MUST begin with the characters ST so you should not change this. * ``CAS_PROXY_TICKET_PREFIX``: Prefix of proxy ticket. The default is ``"PT"``. * ``CAS_PROXY_GRANTING_TICKET_PREFIX``: Prefix of proxy granting ticket. The default is ``"PGT"``. * ``CAS_PROXY_GRANTING_TICKET_IOU_PREFIX``: Prefix of proxy granting ticket IOU. The default is ``"PGTIOU"``. Mysql backend settings ---------------------- Deprecated, see the `Sql backend settings`_. Only usefull if you are using the mysql authentication backend: * ``CAS_SQL_HOST``: Host for the SQL server. The default is ``"localhost"``. * ``CAS_SQL_USERNAME``: Username for connecting to the SQL server. * ``CAS_SQL_PASSWORD``: Password for connecting to the SQL server. * ``CAS_SQL_DBNAME``: Database name. * ``CAS_SQL_DBCHARSET``: Database charset. The default is ``"utf8"`` * ``CAS_SQL_USER_QUERY``: The query performed upon user authentication. The username must be in field ``username``, the password in ``password``, additional fields are used as the user attributes. The default is ``"SELECT user AS username, pass AS password, users.* FROM users WHERE user = %s"`` * ``CAS_SQL_PASSWORD_CHECK``: The method used to check the user password. Must be one of the following: * ``"crypt"`` (see ), the password in the database should begin this $ * ``"ldap"`` (see https://tools.ietf.org/id/draft-stroeder-hashed-userpassword-values-01.html) the password in the database must begin with one of {MD5}, {SMD5}, {SHA}, {SSHA}, {SHA256}, {SSHA256}, {SHA384}, {SSHA384}, {SHA512}, {SSHA512}, {CRYPT}. * ``"hex_HASH_NAME"`` with ``HASH_NAME`` in md5, sha1, sha224, sha256, sha384, sha512. The hashed password in the database is compare to the hexadecimal digest of the clear password hashed with the corresponding algorithm. * ``"plain"``, the password in the database must be in clear. The default is ``"crypt"``. Sql backend settings -------------------- Only usefull if you are using the sql authentication backend. You must add a ``"cas_server"`` database to `settings.DATABASES `__ as defined in the django documentation. It is then the database use by the sql backend. * ``CAS_SQL_USER_QUERY``: The query performed upon user authentication. The username must be in field ``username``, the password in ``password``, additional fields are used as the user attributes. The default is ``"SELECT user AS username, pass AS password, users.* FROM users WHERE user = %s"`` * ``CAS_SQL_PASSWORD_CHECK``: The method used to check the user password. Must be one of the following: * ``"crypt"`` (see ), the password in the database should begin this $ * ``"ldap"`` (see https://tools.ietf.org/id/draft-stroeder-hashed-userpassword-values-01.html) the password in the database must begin with one of {MD5}, {SMD5}, {SHA}, {SSHA}, {SHA256}, {SSHA256}, {SHA384}, {SSHA384}, {SHA512}, {SSHA512}, {CRYPT}. * ``"hex_HASH_NAME"`` with ``HASH_NAME`` in md5, sha1, sha224, sha256, sha384, sha512. The hashed password in the database is compare to the hexadecimal digest of the clear password hashed with the corresponding algorithm. * ``"plain"``, the password in the database must be in clear. The default is ``"crypt"``. * ``CAS_SQL_PASSWORD_CHARSET``: Charset the SQL users passwords was hash with. This is needed to encode the user sended password before hashing it for comparison. The default is ``"utf-8"``. Ldap backend settings --------------------- Only usefull if you are using the ldap authentication backend: * ``CAS_LDAP_SERVER``: Address of the LDAP server. The default is ``"localhost"``. * ``CAS_LDAP_USER``: User bind address, for example ``"cn=admin,dc=crans,dc=org"`` for connecting to the LDAP server. * ``CAS_LDAP_PASSWORD``: Password for connecting to the LDAP server. * ``CAS_LDAP_BASE_DN``: LDAP search base DN, for example ``"ou=data,dc=crans,dc=org"``. * ``CAS_LDAP_USER_QUERY``: Search filter for searching user by username. User inputed usernames are escaped using ``ldap3.utils.conv.escape_bytes``. The default is ``"(uid=%s)"`` * ``CAS_LDAP_USERNAME_ATTR``: Attribute used for users usernames. The default is ``"uid"`` * ``CAS_LDAP_PASSWORD_ATTR``: Attribute used for users passwords. The default is ``"userPassword"`` * ``CAS_LDAP_PASSWORD_CHECK``: The method used to check the user password. Must be one of the following: * ``"crypt"`` (see ), the password in the database should begin this $ * ``"ldap"`` (see https://tools.ietf.org/id/draft-stroeder-hashed-userpassword-values-01.html) the password in the database must begin with one of {MD5}, {SMD5}, {SHA}, {SSHA}, {SHA256}, {SSHA256}, {SHA384}, {SSHA384}, {SHA512}, {SSHA512}, {CRYPT}. * ``"hex_HASH_NAME"`` with ``HASH_NAME`` in md5, sha1, sha224, sha256, sha384, sha512. The hashed password in the database is compare to the hexadecimal digest of the clear password hashed with the corresponding algorithm. * ``"plain"``, the password in the database must be in clear. * ``"bind``, the user credentials are used to bind to the ldap database and retreive the user attribute. In this mode, the settings ``CAS_LDAP_PASSWORD_ATTR`` and ``CAS_LDAP_PASSWORD_CHARSET`` are ignored, and it is the ldap server that perform password check. The counterpart is that the user attributes are only available upon user password check and so are cached for later use. All the other modes directly fetch the user attributes from the database whenever there are needed. This mean that is you use this mode, they can be some difference between the attributes in database and the cached ones if changes happend in the database after the user authentiate. See the parameter ``CAS_TGT_VALIDITY`` to force user to reauthenticate periodically. The default is ``"ldap"``. * ``CAS_LDAP_PASSWORD_CHARSET``: Charset the LDAP users passwords was hash with. This is needed to encode the user sended password before hashing it for comparison. The default is ``"utf-8"``. Test backend settings --------------------- Only usefull if you are using the test authentication backend: * ``CAS_TEST_USER``: Username of the test user. The default is ``"test"``. * ``CAS_TEST_PASSWORD``: Password of the test user. The default is ``"test"``. * ``CAS_TEST_ATTRIBUTES``: Attributes of the test user. The default is ``{'nom': 'Nymous', 'prenom': 'Ano', 'email': 'anonymous@example.net', 'alias': ['demo1', 'demo2']}``. Authentication backend ====================== ``django-cas-server`` comes with some authentication backends: * dummy backend ``cas_server.auth.DummyAuthUser``: all authentication attempt fails. * test backend ``cas_server.auth.TestAuthUser``: username, password and returned attributes for the user are defined by the ``CAS_TEST_*`` settings. * django backend ``cas_server.auth.DjangoAuthUser``: Users are authenticated against django users system. This is the default backend. The returned attributes are the fields available on the user model. * mysql backend ``cas_server.auth.MysqlAuthUser``: Deprecated, use the sql backend instead. see the `Mysql backend settings`_ section. The returned attributes are those return by sql query ``CAS_SQL_USER_QUERY``. * sql backend ``cas_server.auth.SqlAuthUser``: see the `Sql backend settings`_ section. The returned attributes are those return by sql query ``CAS_SQL_USER_QUERY``. * ldap backend ``cas_server.auth.LdapAuthUser``: see the `Ldap backend settings`_ section. The returned attributes are those of the ldap node returned by the query filter ``CAS_LDAP_USER_QUERY``. * federated backend ``cas_server.auth.CASFederateAuth``: It is automatically used then ``CAS_FEDERATE`` is ``True``. You should not set it manually without setting ``CAS_FEDERATE`` to ``True``. Logs ==== ``django-cas-server`` logs most of its actions. To enable login, you must set the ``LOGGING`` (https://docs.djangoproject.com/en/stable/topics/logging) variable in ``settings.py``. Users successful actions (login, logout) are logged with the level ``INFO``, failures are logged with the level ``WARNING`` and user attributes transmitted to a service are logged with the level ``DEBUG``. For example to log to syslog you can use : .. code-block:: python LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'cas_syslog': { 'format': 'cas: %(levelname)s %(message)s' }, }, 'handlers': { 'cas_syslog': { 'level': 'INFO', 'class': 'logging.handlers.SysLogHandler', 'address': '/dev/log', 'formatter': 'cas_syslog', }, }, 'loggers': { 'cas_server': { 'handlers': ['cas_syslog'], 'level': 'INFO', 'propagate': True, }, }, } Or to log to a file: .. code-block:: python LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'cas_file': { 'format': '%(asctime)s %(levelname)s %(message)s' }, }, 'handlers': { 'cas_file': { 'level': 'INFO', 'class': 'logging.FileHandler', 'filename': '/tmp/cas_server.log', 'formatter': 'cas_file', }, }, 'loggers': { 'cas_server': { 'handlers': ['cas_file'], 'level': 'INFO', 'propagate': True, }, }, } Service Patterns ================ In a CAS context, ``Service`` refers to the application the client is trying to access. By extension we use ``service`` for the URL of such an application. By default, ``django-cas-server`` do not allow any service to use the CAS to authenticate users. In order to allow services, you need to connect to the django admin interface using a django superuser, and add a first service pattern. A service pattern comes with 9 fields: * ``Position``: an integer used to change the order in which services are matched against service patterns. * ``Name``: the name of the service pattern. It will be displayed to the users asking for a ticket for a service matching this service pattern on the login page. * ``Pattern``: a regular expression used to match services. * ``User field``: the user attribute to use as username for services matching this service pattern. Leave it empty to use the login name. * ``Restrict username``: if checked, only login name defined below are allowed to get tickets for services matching this service pattern. * ``Proxy``: if checked, allow the creation of Proxy Ticket for services matching this service pattern. Otherwise, only Service Ticket will be created. * ``Proxy callback``: if checked, services matching this service pattern are allowed to retrieve Proxy Granting Ticket. A service with a Proxy Granting Ticket can get Proxy Ticket for other services. Hence you must only check this for trusted services that need it. (For instance, a webmail needs Proxy Ticket to authenticate himself as the user to the imap server). * ``Single log out``: Check it to send Single Log Out requests to authenticated services matching this service pattern. SLO requests are send to all services the user is authenticated to then the user disconnect. * ``Single log out callback``: The http(s) URL to POST the SLO requests. If empty, the service URL is used. This field is useful to allow non http services (imap, smtp, ftp) to handle SLO requests. A service pattern has 4 associated models: * ``Usernames``: a list of username associated with the ``Restrict username`` field * ``Replace attribut names``: a list of user attributes to send to the service. Choose the name used for sending the attribute by setting ``Remplacement`` or leave it empty to leave it unchanged. * ``Replace attribut values``: a list of sent user attributes for which value needs to be tweak. Replace the attribute value by the string obtained by replacing the leftmost non-overlapping occurrences of ``pattern`` in string by ``replace``. In ``replace`` backslash escapes are processed. Matched groups are captures by \1, \2, etc. * ``Filter attribut values``: a list of user attributes for which value needs to match a regular expression. For instance, service A may need an email address, and you only want user with an email address to connect to it. To do so, put ``email`` in ``Attribute`` and ``.*`` in ``pattern``. Then a user ask a ticket for a service, the service URL is compare against each service patterns sorted by ``position``. The first service pattern that matches the service URL is chosen. Hence, you should give low ``position`` to very specific patterns like ``^https://www\.example\.com(/.*)?$`` and higher ``position`` to generic patterns like ``^https://.*``. So the service URL ``https://www.examle.com`` will use the service pattern for ``^https://www\.example\.com(/.*)?$`` and not the one for ``^https://.*``. Federation mode =============== ``django-cas-server`` comes with a federation mode. Then ``CAS_FEDERATE`` is ``True``, user are invited to choose an identity provider on the login page, then, they are redirected to the provider CAS to authenticate. This provider transmit to ``django-cas-server`` the user username and attributes. The user is now logged in on ``django-cas-server`` and can use services using ``django-cas-server`` as CAS. In federation mode, the user attributes are cached upon user authentication. See the settings ``CAS_TGT_VALIDITY`` to force users to reauthenticate periodically and allow ``django-cas-server`` to refresh cached attributes. The list of allowed identity providers is defined using the django admin application. With the development server started, visit http://127.0.0.1:8000/admin/ to add identity providers. An identity provider comes with 5 fields: * ``Position``: an integer used to tweak the order in which identity providers are displayed on the login page. Identity providers are sorted using position first, then, on equal position, using ``verbose name`` and then, on equal ``verbose name``, using ``suffix``. * ``Suffix``: the suffix that will be append to the username returned by the identity provider. It must be unique. * ``Server url``: the URL to the identity provider CAS. For instance, if you are using ``https://cas.example.org/login`` to authenticate on the CAS, the ``server url`` is ``https://cas.example.org`` * ``CAS protocol version``: the version of the CAS protocol to use to contact the identity provider. The default is version 3. * ``Verbose name``: the name used on the login page to display the identity provider. * ``Display``: a boolean controlling the display of the identity provider on the login page. Beware that this do not disable the identity provider, it just hide it on the login page. User will always be able to log in using this provider by fetching ``/federate/provider_suffix``. In federation mode, ``django-cas-server`` build user's username as follow: ``provider_returned_username@provider_suffix``. Choose the provider returned username for ``django-cas-server`` and the provider suffix in order to make sense, as this built username is likely to be displayed to end users in applications. Then using federate mode, you should add one command to a daily crontab: ``cas_clean_federate``. This command clean the local cache of federated user from old unused users. You could for example do as bellow:: 10 0 * * * cas-user /path/to/project/manage.py cas_clean_federate .. |travis| image:: https://badges.genua.fr/travis/nitmir/django-cas-server/master.svg :target: https://travis-ci.org/nitmir/django-cas-server .. |pypi_version| image:: https://badges.genua.fr/pypi/v/django-cas-server.svg :target: https://pypi.org/project/django-cas-server/ .. |github_version| image:: https://badges.genua.fr/github/tag/nitmir/django-cas-server.svg?label=github :target: https://github.com/nitmir/django-cas-server/releases/latest .. |licence| image:: https://badges.genua.fr/pypi/l/django-cas-server.svg :target: https://www.gnu.org/licenses/gpl-3.0.html .. |codacy| image:: https://badges.genua.fr/codacy/grade/255c21623d6946ef8802fa7995b61366/master.svg :target: https://www.codacy.com/app/valentin-samir/django-cas-server .. |coverage| image:: https://intranet.genua.fr/coverage/badge/django-cas-server/master.svg :target: https://badges.genua.fr/coverage/django-cas-server/master .. |doc| image:: https://badges.genua.fr/local/readthedocs/?version=latest :target: http://django-cas-server.readthedocs.io Keywords: django,cas,cas3,server,sso,single sign-on,authentication,auth Platform: UNKNOWN Classifier: Environment :: Web Environment Classifier: Development Status :: 5 - Production/Stable Classifier: Framework :: Django Classifier: Framework :: Django :: 1.11 Classifier: Framework :: Django :: 2.0 Classifier: Framework :: Django :: 2.1 Classifier: Intended Audience :: Developers Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3) Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: System :: Systems Administration :: Authentication/Directory django-cas-server-1.1.0/requirements.txt0000644000175000017500000000013513436457341022002 0ustar valentinvalentin00000000000000Django >= 1.11,<2.2 setuptools>=5.5 requests>=2.4 requests_futures>=0.9.5 lxml>=3.4 six>=1.8 django-cas-server-1.1.0/docs/0000755000175000017500000000000013436457571017454 5ustar valentinvalentin00000000000000django-cas-server-1.1.0/docs/index.rst0000644000175000017500000000077512757406463021325 0ustar valentinvalentin00000000000000.. django-cas-server documentation master file, created by sphinx-quickstart on Tue Jul 5 12:11:50 2016. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. django-cas-server documentation =============================== Contents: .. toctree:: :maxdepth: 3 README package/cas_server .. toctree:: :maxdepth: 2 CHANGELOG Indices and tables ================== * :ref:`genindex` .. * :ref:`modindex` .. * :ref:`search` django-cas-server-1.1.0/docs/CHANGELOG.rst0000644000175000017500000000003612757406463021473 0ustar valentinvalentin00000000000000.. include:: ../CHANGELOG.rst django-cas-server-1.1.0/docs/README.rst0000644000175000017500000000003412757022243021125 0ustar valentinvalentin00000000000000.. include:: ../README.rst django-cas-server-1.1.0/docs/package/0000755000175000017500000000000013436457571021047 5ustar valentinvalentin00000000000000django-cas-server-1.1.0/docs/package/cas_server.auth.rst0000644000175000017500000000021012757022243024653 0ustar valentinvalentin00000000000000cas_server.auth module ====================== .. automodule:: cas_server.auth :members: :undoc-members: :show-inheritance: django-cas-server-1.1.0/docs/package/cas_server.models.rst0000644000175000017500000000016712757022243025210 0ustar valentinvalentin00000000000000cas_server.models module ======================== .. automodule:: cas_server.models :members: :undoc-members: django-cas-server-1.1.0/docs/package/cas_server.apps.rst0000644000175000017500000000016112757022243024662 0ustar valentinvalentin00000000000000cas_server.apps module ====================== .. automodule:: cas_server.apps :members: :undoc-members: django-cas-server-1.1.0/docs/package/cas_server.templatetags.cas_server.rst0000644000175000017500000000030112757022243030540 0ustar valentinvalentin00000000000000cas_server.templatetags.cas_server module ========================================= .. automodule:: cas_server.templatetags.cas_server :members: :undoc-members: :show-inheritance: django-cas-server-1.1.0/docs/package/cas_server.views.rst0000644000175000017500000000021312757022243025052 0ustar valentinvalentin00000000000000cas_server.views module ======================= .. automodule:: cas_server.views :members: :undoc-members: :show-inheritance: django-cas-server-1.1.0/docs/package/cas_server.urls.rst0000644000175000017500000000021012757022243024677 0ustar valentinvalentin00000000000000cas_server.urls module ====================== .. automodule:: cas_server.urls :members: :undoc-members: :show-inheritance: django-cas-server-1.1.0/docs/package/cas_server.admin.rst0000644000175000017500000000016512757022243025013 0ustar valentinvalentin00000000000000cas_server.admin module ======================= .. automodule:: cas_server.admin :members: :undoc-members: django-cas-server-1.1.0/docs/package/modules.rst0000644000175000017500000000010312757022243023230 0ustar valentinvalentin00000000000000cas_server ========== .. toctree:: :maxdepth: 4 cas_server django-cas-server-1.1.0/docs/package/cas_server.default_settings.rst0000644000175000017500000000025412757022243027266 0ustar valentinvalentin00000000000000cas_server.default_settings module ================================== .. automodule:: cas_server.default_settings :members: :undoc-members: :show-inheritance: django-cas-server-1.1.0/docs/package/cas_server.forms.rst0000644000175000017500000000014012757022243025042 0ustar valentinvalentin00000000000000cas_server.forms module ======================= .. automodule:: cas_server.forms :members: django-cas-server-1.1.0/docs/package/cas_server.cas.rst0000644000175000017500000000020512757022243024464 0ustar valentinvalentin00000000000000cas_server.cas module ===================== .. automodule:: cas_server.cas :members: :undoc-members: :show-inheritance: django-cas-server-1.1.0/docs/package/cas_server.federate.rst0000644000175000017500000000022412757022243025476 0ustar valentinvalentin00000000000000cas_server.federate module ========================== .. automodule:: cas_server.federate :members: :undoc-members: :show-inheritance: django-cas-server-1.1.0/docs/package/cas_server.templatetags.rst0000644000175000017500000000041712757022243026415 0ustar valentinvalentin00000000000000cas_server.templatetags package =============================== Submodules ---------- .. toctree:: cas_server.templatetags.cas_server Module contents --------------- .. automodule:: cas_server.templatetags :members: :undoc-members: :show-inheritance: django-cas-server-1.1.0/docs/package/cas_server.utils.rst0000644000175000017500000000021312757022243025055 0ustar valentinvalentin00000000000000cas_server.utils module ======================= .. automodule:: cas_server.utils :members: :undoc-members: :show-inheritance: django-cas-server-1.1.0/docs/package/cas_server.rst0000644000175000017500000000075412757022243023730 0ustar valentinvalentin00000000000000cas_server package ================== Subpackages ----------- .. toctree:: cas_server.templatetags Submodules ---------- .. toctree:: cas_server.admin cas_server.apps cas_server.auth cas_server.cas cas_server.default_settings cas_server.federate cas_server.forms cas_server.models cas_server.urls cas_server.utils cas_server.views Module contents --------------- .. automodule:: cas_server :members: :undoc-members: :show-inheritance: django-cas-server-1.1.0/docs/conf.py0000644000175000017500000002512212757022243020742 0ustar valentinvalentin00000000000000# -*- coding: utf-8 -*- # # django-cas-server documentation build configuration file, created by # sphinx-quickstart on Tue Jul 5 12:11:50 2016. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import os import sys sys.path.insert(0, os.path.abspath('.')) sys.path.append(os.path.abspath('..')) sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "_ext"))) import setup as mysetup os.environ['DJANGO_SETTINGS_MODULE'] = 'cas_server.tests.settings' import django django.setup() # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'djangodocs', 'sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.coverage', 'sphinx.ext.viewcode', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = '.rst' # The encoding of source files. # # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = u'django-cas-server' copyright = u'2016, Valentin Samir' author = u'Valentin Samir' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = mysetup.VERSION # The full version, including alpha/beta/rc tags. release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # # today = '' # # Else, today_fmt is used as the format for a strftime call. # # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] # The reST default role (used for this markup: `text`) to use for all # documents. # # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'sphinx' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # #html_theme = 'alabaster' html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. # " v documentation" by default. # # html_title = u'django-cas-server v5.0' # A shorter title for the navigation bar. Default is the same as html_title. # # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # # html_logo = None # The name of an image file (relative to this directory) to use as a favicon of # the docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # # html_extra_path = [] # If not None, a 'Last updated on:' timestamp is inserted at every page # bottom, using the given strftime format. # The empty string is equivalent to '%b %d, %Y'. # # html_last_updated_fmt = None # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # # html_additional_pages = {} # If false, no module index is generated. # # html_domain_indices = True # If false, no index is generated. # # html_use_index = True # If true, the index is split into individual pages for each letter. # # html_split_index = False # If true, links to the reST sources are added to the pages. # # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Language to be used for generating the HTML full-text search index. # Sphinx supports the following languages: # 'da', 'de', 'en', 'es', 'fi', 'fr', 'hu', 'it', 'ja' # 'nl', 'no', 'pt', 'ro', 'ru', 'sv', 'tr', 'zh' # # html_search_language = 'en' # A dictionary with options for the search language support, empty by default. # 'ja' uses this config value. # 'zh' user can custom change `jieba` dictionary path. # # html_search_options = {'type': 'default'} # The name of a javascript file (relative to the configuration directory) that # implements a search results scorer. If empty, the default will be used. # # html_search_scorer = 'scorer.js' # Output file base name for HTML help builder. htmlhelp_basename = 'django-cas-serverdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, 'django-cas-server.tex', u'django-cas-server Documentation', u'Valentin Samir', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # # latex_use_parts = False # If true, show page references after internal links. # # latex_show_pagerefs = False # If true, show URL addresses after external links. # # latex_show_urls = False # Documents to append as an appendix to all manuals. # # latex_appendices = [] # If false, no module index is generated. # # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ (master_doc, 'django-cas-server', u'django-cas-server Documentation', [author], 1) ] # If true, show URL addresses after external links. # # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ (master_doc, 'django-cas-server', u'django-cas-server Documentation', author, 'django-cas-server', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. # # texinfo_appendices = [] # If false, no module index is generated. # # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # # texinfo_no_detailmenu = False # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = { "python": ('https://docs.python.org/', None), "django": ('https://docs.djangoproject.com/en/1.9/', 'django.inv'), } autodoc_member_order = 'bysource' locale_dirs = ['../test_venv/lib/python2.7/site-packages/django/conf/locale/'] def _download_django_inv(): import requests with open(_download_django_inv.path, 'w') as f: r = requests.get("https://docs.djangoproject.com/en/1.9/_objects") f.write(r.content) _download_django_inv.path = os.path.abspath(os.path.join(os.path.dirname(__file__), "django.inv")) if not os.path.isfile(_download_django_inv.path): _download_django_inv() django-cas-server-1.1.0/docs/_ext/0000755000175000017500000000000013436457571020413 5ustar valentinvalentin00000000000000django-cas-server-1.1.0/docs/_ext/djangodocs.py0000644000175000017500000002470112757022243023071 0ustar valentinvalentin00000000000000""" Sphinx plugins for Django documentation. """ import json import os import re from docutils import nodes from docutils.parsers.rst import directives from sphinx import addnodes from sphinx.builders.html import StandaloneHTMLBuilder from sphinx.domains.std import Cmdoption from sphinx.util.compat import Directive from sphinx.util.console import bold from sphinx.util.nodes import set_source_info from sphinx.writers.html import SmartyPantsHTMLTranslator # RE for option descriptions without a '--' prefix simple_option_desc_re = re.compile( r'([-_a-zA-Z0-9]+)(\s*.*?)(?=,\s+(?:/|-|--)|$)') def setup(app): app.add_crossref_type( directivename="setting", rolename="setting", indextemplate="pair: %s; setting", ) app.add_crossref_type( directivename="templatetag", rolename="ttag", indextemplate="pair: %s; template tag" ) app.add_crossref_type( directivename="templatefilter", rolename="tfilter", indextemplate="pair: %s; template filter" ) app.add_crossref_type( directivename="fieldlookup", rolename="lookup", indextemplate="pair: %s; field lookup type", ) app.add_description_unit( directivename="django-admin", rolename="djadmin", indextemplate="pair: %s; django-admin command", parse_node=parse_django_admin_node, ) app.add_directive('django-admin-option', Cmdoption) app.add_config_value('django_next_version', '0.0', True) app.add_directive('versionadded', VersionDirective) app.add_directive('versionchanged', VersionDirective) app.add_builder(DjangoStandaloneHTMLBuilder) # register the snippet directive app.add_directive('snippet', SnippetWithFilename) # register a node for snippet directive so that the xml parser # knows how to handle the enter/exit parsing event app.add_node(snippet_with_filename, html=(visit_snippet, depart_snippet_literal), latex=(visit_snippet_latex, depart_snippet_latex), man=(visit_snippet_literal, depart_snippet_literal), text=(visit_snippet_literal, depart_snippet_literal), texinfo=(visit_snippet_literal, depart_snippet_literal)) return {'parallel_read_safe': True} class snippet_with_filename(nodes.literal_block): """ Subclass the literal_block to override the visit/depart event handlers """ pass def visit_snippet_literal(self, node): """ default literal block handler """ self.visit_literal_block(node) def depart_snippet_literal(self, node): """ default literal block handler """ self.depart_literal_block(node) def visit_snippet(self, node): """ HTML document generator visit handler """ lang = self.highlightlang linenos = node.rawsource.count('\n') >= self.highlightlinenothreshold - 1 fname = node['filename'] highlight_args = node.get('highlight_args', {}) if 'language' in node: # code-block directives lang = node['language'] highlight_args['force'] = True if 'linenos' in node: linenos = node['linenos'] def warner(msg): self.builder.warn(msg, (self.builder.current_docname, node.line)) highlighted = self.highlighter.highlight_block(node.rawsource, lang, warn=warner, linenos=linenos, **highlight_args) starttag = self.starttag(node, 'div', suffix='', CLASS='highlight-%s snippet' % lang) self.body.append(starttag) self.body.append('
%s
\n''' % (fname,)) self.body.append(highlighted) self.body.append('\n') raise nodes.SkipNode def visit_snippet_latex(self, node): """ Latex document generator visit handler """ code = node.rawsource.rstrip('\n') lang = self.hlsettingstack[-1][0] linenos = code.count('\n') >= self.hlsettingstack[-1][1] - 1 fname = node['filename'] highlight_args = node.get('highlight_args', {}) if 'language' in node: # code-block directives lang = node['language'] highlight_args['force'] = True if 'linenos' in node: linenos = node['linenos'] def warner(msg): self.builder.warn(msg, (self.curfilestack[-1], node.line)) hlcode = self.highlighter.highlight_block(code, lang, warn=warner, linenos=linenos, **highlight_args) self.body.append( '\n{\\colorbox[rgb]{0.9,0.9,0.9}' '{\\makebox[\\textwidth][l]' '{\\small\\texttt{%s}}}}\n' % ( # Some filenames have '_', which is special in latex. fname.replace('_', r'\_'), ) ) if self.table: hlcode = hlcode.replace('\\begin{Verbatim}', '\\begin{OriginalVerbatim}') self.table.has_problematic = True self.table.has_verbatim = True hlcode = hlcode.rstrip()[:-14] # strip \end{Verbatim} hlcode = hlcode.rstrip() + '\n' self.body.append('\n' + hlcode + '\\end{%sVerbatim}\n' % (self.table and 'Original' or '')) # Prevent rawsource from appearing in output a second time. raise nodes.SkipNode def depart_snippet_latex(self, node): """ Latex document generator depart handler. """ pass class SnippetWithFilename(Directive): """ The 'snippet' directive that allows to add the filename (optional) of a code snippet in the document. This is modeled after CodeBlock. """ has_content = True optional_arguments = 1 option_spec = {'filename': directives.unchanged_required} def run(self): code = '\n'.join(self.content) literal = snippet_with_filename(code, code) if self.arguments: literal['language'] = self.arguments[0] literal['filename'] = self.options['filename'] set_source_info(self, literal) return [literal] class VersionDirective(Directive): has_content = True required_arguments = 1 optional_arguments = 1 final_argument_whitespace = True option_spec = {} def run(self): if len(self.arguments) > 1: msg = """Only one argument accepted for directive '{directive_name}::'. Comments should be provided as content, not as an extra argument.""".format(directive_name=self.name) raise self.error(msg) env = self.state.document.settings.env ret = [] node = addnodes.versionmodified() ret.append(node) if self.arguments[0] == env.config.django_next_version: node['version'] = "Development version" else: node['version'] = self.arguments[0] node['type'] = self.name if self.content: self.state.nested_parse(self.content, self.content_offset, node) env.note_versionchange(node['type'], node['version'], node, self.lineno) return ret class DjangoHTMLTranslator(SmartyPantsHTMLTranslator): """ Django-specific reST to HTML tweaks. """ # Don't use border=1, which docutils does by default. def visit_table(self, node): self.context.append(self.compact_p) self.compact_p = True self._table_row_index = 0 # Needed by Sphinx self.body.append(self.starttag(node, 'table', CLASS='docutils')) def depart_table(self, node): self.compact_p = self.context.pop() self.body.append('\n') def visit_desc_parameterlist(self, node): self.body.append('(') # by default sphinx puts around the "(" self.first_param = 1 self.optional_param_level = 0 self.param_separator = node.child_text_separator self.required_params_left = sum([isinstance(c, addnodes.desc_parameter) for c in node.children]) def depart_desc_parameterlist(self, node): self.body.append(')') # # Turn the "new in version" stuff (versionadded/versionchanged) into a # better callout -- the Sphinx default is just a little span, # which is a bit less obvious that I'd like. # # FIXME: these messages are all hardcoded in English. We need to change # that to accommodate other language docs, but I can't work out how to make # that work. # version_text = { 'versionchanged': 'Changed in Django %s', 'versionadded': 'New in Django %s', } def visit_versionmodified(self, node): self.body.append( self.starttag(node, 'div', CLASS=node['type']) ) version_text = self.version_text.get(node['type']) if version_text: title = "%s%s" % ( version_text % node['version'], ":" if len(node) else "." ) self.body.append('%s ' % title) def depart_versionmodified(self, node): self.body.append("\n") # Give each section a unique ID -- nice for custom CSS hooks def visit_section(self, node): old_ids = node.get('ids', []) node['ids'] = ['s-' + i for i in old_ids] node['ids'].extend(old_ids) SmartyPantsHTMLTranslator.visit_section(self, node) node['ids'] = old_ids def parse_django_admin_node(env, sig, signode): command = sig.split(' ')[0] env.ref_context['std:program'] = command title = "django-admin %s" % sig signode += addnodes.desc_name(title, title) return command class DjangoStandaloneHTMLBuilder(StandaloneHTMLBuilder): """ Subclass to add some extra things we need. """ name = 'djangohtml' def finish(self): super(DjangoStandaloneHTMLBuilder, self).finish() self.info(bold("writing templatebuiltins.js...")) xrefs = self.env.domaindata["std"]["objects"] templatebuiltins = { "ttags": [n for ((t, n), (l, a)) in xrefs.items() if t == "templatetag" and l == "ref/templates/builtins"], "tfilters": [n for ((t, n), (l, a)) in xrefs.items() if t == "templatefilter" and l == "ref/templates/builtins"], } outfilename = os.path.join(self.outdir, "templatebuiltins.js") with open(outfilename, 'w') as fp: fp.write('var django_template_builtins = ') json.dump(templatebuiltins, fp) fp.write(';\n') django-cas-server-1.1.0/docs/Makefile0000644000175000017500000001674212757022243021113 0ustar valentinvalentin00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build PAPER = BUILDDIR = _build # Internal variables. PAPEROPT_a4 = -D latex_paper_size=a4 PAPEROPT_letter = -D latex_paper_size=letter ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . # the i18n builder cannot share the environment and doctrees with the others I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . .PHONY: help help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @echo " singlehtml to make a single large HTML file" @echo " pickle to make pickle files" @echo " json to make JSON files" @echo " htmlhelp to make HTML files and a HTML help project" @echo " qthelp to make HTML files and a qthelp project" @echo " applehelp to make an Apple Help Book" @echo " devhelp to make HTML files and a Devhelp project" @echo " epub to make an epub" @echo " epub3 to make an epub3" @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @echo " latexpdf to make LaTeX files and run them through pdflatex" @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" @echo " text to make text files" @echo " man to make manual pages" @echo " texinfo to make Texinfo files" @echo " info to make Texinfo files and run them through makeinfo" @echo " gettext to make PO message catalogs" @echo " changes to make an overview of all changed/added/deprecated items" @echo " xml to make Docutils-native XML files" @echo " pseudoxml to make pseudoxml-XML files for display purposes" @echo " linkcheck to check all external links for integrity" @echo " doctest to run all doctests embedded in the documentation (if enabled)" @echo " coverage to run coverage check of the documentation (if enabled)" @echo " dummy to check syntax errors of document sources" .PHONY: clean clean: rm -rf $(BUILDDIR)/* .PHONY: html html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." .PHONY: dirhtml dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." .PHONY: singlehtml singlehtml: $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml @echo @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." .PHONY: pickle pickle: $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle @echo @echo "Build finished; now you can process the pickle files." .PHONY: json json: $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json @echo @echo "Build finished; now you can process the JSON files." .PHONY: htmlhelp htmlhelp: $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp @echo @echo "Build finished; now you can run HTML Help Workshop with the" \ ".hhp project file in $(BUILDDIR)/htmlhelp." .PHONY: qthelp qthelp: $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/django-cas-server.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/django-cas-server.qhc" .PHONY: applehelp applehelp: $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp @echo @echo "Build finished. The help book is in $(BUILDDIR)/applehelp." @echo "N.B. You won't be able to view it unless you put it in" \ "~/Library/Documentation/Help or install it in your application" \ "bundle." .PHONY: devhelp devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" @echo "# mkdir -p $$HOME/.local/share/devhelp/django-cas-server" @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/django-cas-server" @echo "# devhelp" .PHONY: epub epub: $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub @echo @echo "Build finished. The epub file is in $(BUILDDIR)/epub." .PHONY: epub3 epub3: $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3 @echo @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3." .PHONY: latex latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make' in that directory to run these through (pdf)latex" \ "(use \`make latexpdf' here to do that automatically)." .PHONY: latexpdf latexpdf: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through pdflatex..." $(MAKE) -C $(BUILDDIR)/latex all-pdf @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: latexpdfja latexpdfja: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo "Running LaTeX files through platex and dvipdfmx..." $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." .PHONY: text text: $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text @echo @echo "Build finished. The text files are in $(BUILDDIR)/text." .PHONY: man man: $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man @echo @echo "Build finished. The manual pages are in $(BUILDDIR)/man." .PHONY: texinfo texinfo: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." @echo "Run \`make' in that directory to run these through makeinfo" \ "(use \`make info' here to do that automatically)." .PHONY: info info: $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo @echo "Running Texinfo files through makeinfo..." make -C $(BUILDDIR)/texinfo info @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." .PHONY: gettext gettext: $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale @echo @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." .PHONY: changes changes: $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes @echo @echo "The overview file is in $(BUILDDIR)/changes." .PHONY: linkcheck linkcheck: $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck @echo @echo "Link check complete; look for any errors in the above output " \ "or in $(BUILDDIR)/linkcheck/output.txt." .PHONY: doctest doctest: $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest @echo "Testing of doctests in the sources finished, look at the " \ "results in $(BUILDDIR)/doctest/output.txt." .PHONY: coverage coverage: $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage @echo "Testing of coverage in the sources finished, look at the " \ "results in $(BUILDDIR)/coverage/python.txt." .PHONY: xml xml: $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml @echo @echo "Build finished. The XML files are in $(BUILDDIR)/xml." .PHONY: pseudoxml pseudoxml: $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml @echo @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." .PHONY: dummy dummy: $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy @echo @echo "Build finished. Dummy builder generates no files." django-cas-server-1.1.0/tox.ini0000644000175000017500000000546513436457341020044 0ustar valentinvalentin00000000000000[tox] envlist= flake8, check_rst, py27-django111, py35-django111, py36-django111, py35-django20, py36-django20, py35-django21, py36-django21, [flake8] max-line-length=100 exclude=migrations [base] deps = -r{toxinidir}/requirements-dev.txt [post_cmd] commands= find {toxworkdir} -name '*.pyc' -delete mkdir -p {toxinidir}/tox_logs/ bash -c "mv {toxworkdir}/{envname}/log/* {toxinidir}/tox_logs/" whitelist_externals= find bash mkdir [testenv] commands= py.test -rw {posargs:cas_server/tests/} {[post_cmd]commands} whitelist_externals={[post_cmd]whitelist_externals} [testenv:py27-django17] basepython=python2.7 deps = Django>=1.7,<1.8 {[base]deps} [testenv:py27-django18] basepython=python2.7 deps = Django>=1.8,<1.9 {[base]deps} [testenv:py27-django19] basepython=python2.7 deps = Django>=1.9,<1.10 {[base]deps} [testenv:py27-django110] basepython=python2.7 deps = Django>=1.10,<1.11 {[base]deps} [testenv:py27-django111] basepython=python2.7 deps = Django>=1.11,<1.12 {[base]deps} [testenv:py34-django17] basepython=python3.4 deps = Django>=1.7,<1.8 {[base]deps} [testenv:py34-django18] basepython=python3.4 deps = Django>=1.8,<1.9 {[base]deps} [testenv:py35-django110] basepython=python3.5 deps = Django>=1.10,<1.11 {[base]deps} [testenv:py35-django111] basepython=python3.5 deps = Django>=1.11,<1.12 {[base]deps} [testenv:py36-django111] basepython=python3.6 deps = Django>=1.11,<1.12 {[base]deps} [testenv:py35-django20] basepython=python3.5 deps = Django>=2.0,<2.1 {[base]deps} [testenv:py36-django20] basepython=python3.6 deps = Django>=2.0,<2.1 {[base]deps} [testenv:py35-django21] basepython=python3.5 deps = Django>=2.1,<2.2 {[base]deps} [testenv:py36-django21] basepython=python3.6 deps = Django>=2.1,<2.2 {[base]deps} [testenv:flake8] basepython=python deps=flake8 skip_install=True commands= flake8 {toxinidir}/cas_server {[post_cmd]commands} whitelist_externals={[post_cmd]whitelist_externals} [testenv:check_rst] basepython=python deps= docutils Pygments skip_install=True commands= rst2html.py --strict {toxinidir}/README.rst /dev/null rst2html.py --halt=warning {toxinidir}/CHANGELOG.rst /dev/null {[post_cmd]commands} whitelist_externals={[post_cmd]whitelist_externals} [testenv:coverage] basepython=python passenv= COVERAGE_TOKEN CI_BUILD_REF_NAME TRAVIS_BRANCH TRAVIS_PULL_REQUEST deps= -r{toxinidir}/requirements.txt -r{toxinidir}/requirements-dev.txt skip_install=True commands= py.test --cov=cas_server --cov-report term --cov-report html {toxinidir}/.update_coverage "{toxinidir}" "django-cas-server" {[post_cmd]commands} whitelist_externals={[post_cmd]whitelist_externals} django-cas-server-1.1.0/requirements-dev.txt0000644000175000017500000000027012757022243022550 0ustar valentinvalentin00000000000000setuptools>=5.5 requests>=2.4 requests_futures>=0.9.5 lxml>=3.4 six>=1.8 tox>=1.8.1 pytest>=2.6.4 pytest-django>=2.8.0 pytest-pythonpath>=0.3 pytest-warnings pytest-cov>=2.2.1 mock>=1 django-cas-server-1.1.0/LICENSE0000644000175000017500000010451412632536713017527 0ustar valentinvalentin00000000000000 GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. Preamble The GNU General Public License is a free, copyleft license for software and other kinds of works. The licenses for most software and other practical works are designed to take away your freedom to share and change the works. By contrast, the GNU General Public License is intended to guarantee your freedom to share and change all versions of a program--to make sure it remains free software for all its users. We, the Free Software Foundation, use the GNU General Public License for most of our software; it applies also to any other work released this way by its authors. You can apply it to your programs, too. When we speak of free software, we are referring to freedom, not price. Our General Public Licenses are designed to make sure that you have the freedom to distribute copies of free software (and charge for them if you wish), that you receive source code or can get it if you want it, that you can change the software or use pieces of it in new free programs, and that you know you can do these things. To protect your rights, we need to prevent others from denying you these rights or asking you to surrender the rights. Therefore, you have certain responsibilities if you distribute copies of the software, or if you modify it: responsibilities to respect the freedom of others. For example, if you distribute copies of such a program, whether gratis or for a fee, you must pass on to the recipients the same freedoms that you received. You must make sure that they, too, receive or can get the source code. And you must show them these terms so they know their rights. Developers that use the GNU GPL protect your rights with two steps: (1) assert copyright on the software, and (2) offer you this License giving you legal permission to copy, distribute and/or modify it. For the developers' and authors' protection, the GPL clearly explains that there is no warranty for this free software. For both users' and authors' sake, the GPL requires that modified versions be marked as changed, so that their problems will not be attributed erroneously to authors of previous versions. Some devices are designed to deny users access to install or run modified versions of the software inside them, although the manufacturer can do so. This is fundamentally incompatible with the aim of protecting users' freedom to change the software. The systematic pattern of such abuse occurs in the area of products for individuals to use, which is precisely where it is most unacceptable. Therefore, we have designed this version of the GPL to prohibit the practice for those products. If such problems arise substantially in other domains, we stand ready to extend this provision to those domains in future versions of the GPL, as needed to protect the freedom of users. Finally, every program is threatened constantly by software patents. States should not allow patents to restrict development and use of software on general-purpose computers, but in those that do, we wish to avoid the special danger that patents applied to a free program could make it effectively proprietary. To prevent this, the GPL assures that patents cannot be used to render the program non-free. The precise terms and conditions for copying, distribution and modification follow. TERMS AND CONDITIONS 0. Definitions. "This License" refers to version 3 of the GNU General Public License. "Copyright" also means copyright-like laws that apply to other kinds of works, such as semiconductor masks. "The Program" refers to any copyrightable work licensed under this License. Each licensee is addressed as "you". "Licensees" and "recipients" may be individuals or organizations. To "modify" a work means to copy from or adapt all or part of the work in a fashion requiring copyright permission, other than the making of an exact copy. The resulting work is called a "modified version" of the earlier work or a work "based on" the earlier work. A "covered work" means either the unmodified Program or a work based on the Program. To "propagate" a work means to do anything with it that, without permission, would make you directly or secondarily liable for infringement under applicable copyright law, except executing it on a computer or modifying a private copy. Propagation includes copying, distribution (with or without modification), making available to the public, and in some countries other activities as well. To "convey" a work means any kind of propagation that enables other parties to make or receive copies. Mere interaction with a user through a computer network, with no transfer of a copy, is not conveying. An interactive user interface displays "Appropriate Legal Notices" to the extent that it includes a convenient and prominently visible feature that (1) displays an appropriate copyright notice, and (2) tells the user that there is no warranty for the work (except to the extent that warranties are provided), that licensees may convey the work under this License, and how to view a copy of this License. If the interface presents a list of user commands or options, such as a menu, a prominent item in the list meets this criterion. 1. Source Code. The "source code" for a work means the preferred form of the work for making modifications to it. "Object code" means any non-source form of a work. A "Standard Interface" means an interface that either is an official standard defined by a recognized standards body, or, in the case of interfaces specified for a particular programming language, one that is widely used among developers working in that language. The "System Libraries" of an executable work include anything, other than the work as a whole, that (a) is included in the normal form of packaging a Major Component, but which is not part of that Major Component, and (b) serves only to enable use of the work with that Major Component, or to implement a Standard Interface for which an implementation is available to the public in source code form. A "Major Component", in this context, means a major essential component (kernel, window system, and so on) of the specific operating system (if any) on which the executable work runs, or a compiler used to produce the work, or an object code interpreter used to run it. The "Corresponding Source" for a work in object code form means all the source code needed to generate, install, and (for an executable work) run the object code and to modify the work, including scripts to control those activities. However, it does not include the work's System Libraries, or general-purpose tools or generally available free programs which are used unmodified in performing those activities but which are not part of the work. For example, Corresponding Source includes interface definition files associated with source files for the work, and the source code for shared libraries and dynamically linked subprograms that the work is specifically designed to require, such as by intimate data communication or control flow between those subprograms and other parts of the work. The Corresponding Source need not include anything that users can regenerate automatically from other parts of the Corresponding Source. The Corresponding Source for a work in source code form is that same work. 2. Basic Permissions. All rights granted under this License are granted for the term of copyright on the Program, and are irrevocable provided the stated conditions are met. This License explicitly affirms your unlimited permission to run the unmodified Program. The output from running a covered work is covered by this License only if the output, given its content, constitutes a covered work. This License acknowledges your rights of fair use or other equivalent, as provided by copyright law. You may make, run and propagate covered works that you do not convey, without conditions so long as your license otherwise remains in force. You may convey covered works to others for the sole purpose of having them make modifications exclusively for you, or provide you with facilities for running those works, provided that you comply with the terms of this License in conveying all material for which you do not control copyright. Those thus making or running the covered works for you must do so exclusively on your behalf, under your direction and control, on terms that prohibit them from making any copies of your copyrighted material outside their relationship with you. Conveying under any other circumstances is permitted solely under the conditions stated below. Sublicensing is not allowed; section 10 makes it unnecessary. 3. Protecting Users' Legal Rights From Anti-Circumvention Law. No covered work shall be deemed part of an effective technological measure under any applicable law fulfilling obligations under article 11 of the WIPO copyright treaty adopted on 20 December 1996, or similar laws prohibiting or restricting circumvention of such measures. When you convey a covered work, you waive any legal power to forbid circumvention of technological measures to the extent such circumvention is effected by exercising rights under this License with respect to the covered work, and you disclaim any intention to limit operation or modification of the work as a means of enforcing, against the work's users, your or third parties' legal rights to forbid circumvention of technological measures. 4. Conveying Verbatim Copies. You may convey verbatim copies of the Program's source code as you receive it, in any medium, provided that you conspicuously and appropriately publish on each copy an appropriate copyright notice; keep intact all notices stating that this License and any non-permissive terms added in accord with section 7 apply to the code; keep intact all notices of the absence of any warranty; and give all recipients a copy of this License along with the Program. You may charge any price or no price for each copy that you convey, and you may offer support or warranty protection for a fee. 5. Conveying Modified Source Versions. You may convey a work based on the Program, or the modifications to produce it from the Program, in the form of source code under the terms of section 4, provided that you also meet all of these conditions: a) The work must carry prominent notices stating that you modified it, and giving a relevant date. b) The work must carry prominent notices stating that it is released under this License and any conditions added under section 7. This requirement modifies the requirement in section 4 to "keep intact all notices". c) You must license the entire work, as a whole, under this License to anyone who comes into possession of a copy. This License will therefore apply, along with any applicable section 7 additional terms, to the whole of the work, and all its parts, regardless of how they are packaged. This License gives no permission to license the work in any other way, but it does not invalidate such permission if you have separately received it. d) If the work has interactive user interfaces, each must display Appropriate Legal Notices; however, if the Program has interactive interfaces that do not display Appropriate Legal Notices, your work need not make them do so. A compilation of a covered work with other separate and independent works, which are not by their nature extensions of the covered work, and which are not combined with it such as to form a larger program, in or on a volume of a storage or distribution medium, is called an "aggregate" if the compilation and its resulting copyright are not used to limit the access or legal rights of the compilation's users beyond what the individual works permit. Inclusion of a covered work in an aggregate does not cause this License to apply to the other parts of the aggregate. 6. Conveying Non-Source Forms. You may convey a covered work in object code form under the terms of sections 4 and 5, provided that you also convey the machine-readable Corresponding Source under the terms of this License, in one of these ways: a) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by the Corresponding Source fixed on a durable physical medium customarily used for software interchange. b) Convey the object code in, or embodied in, a physical product (including a physical distribution medium), accompanied by a written offer, valid for at least three years and valid for as long as you offer spare parts or customer support for that product model, to give anyone who possesses the object code either (1) a copy of the Corresponding Source for all the software in the product that is covered by this License, on a durable physical medium customarily used for software interchange, for a price no more than your reasonable cost of physically performing this conveying of source, or (2) access to copy the Corresponding Source from a network server at no charge. c) Convey individual copies of the object code with a copy of the written offer to provide the Corresponding Source. This alternative is allowed only occasionally and noncommercially, and only if you received the object code with such an offer, in accord with subsection 6b. d) Convey the object code by offering access from a designated place (gratis or for a charge), and offer equivalent access to the Corresponding Source in the same way through the same place at no further charge. You need not require recipients to copy the Corresponding Source along with the object code. If the place to copy the object code is a network server, the Corresponding Source may be on a different server (operated by you or a third party) that supports equivalent copying facilities, provided you maintain clear directions next to the object code saying where to find the Corresponding Source. Regardless of what server hosts the Corresponding Source, you remain obligated to ensure that it is available for as long as needed to satisfy these requirements. e) Convey the object code using peer-to-peer transmission, provided you inform other peers where the object code and Corresponding Source of the work are being offered to the general public at no charge under subsection 6d. A separable portion of the object code, whose source code is excluded from the Corresponding Source as a System Library, need not be included in conveying the object code work. A "User Product" is either (1) a "consumer product", which means any tangible personal property which is normally used for personal, family, or household purposes, or (2) anything designed or sold for incorporation into a dwelling. In determining whether a product is a consumer product, doubtful cases shall be resolved in favor of coverage. For a particular product received by a particular user, "normally used" refers to a typical or common use of that class of product, regardless of the status of the particular user or of the way in which the particular user actually uses, or expects or is expected to use, the product. A product is a consumer product regardless of whether the product has substantial commercial, industrial or non-consumer uses, unless such uses represent the only significant mode of use of the product. "Installation Information" for a User Product means any methods, procedures, authorization keys, or other information required to install and execute modified versions of a covered work in that User Product from a modified version of its Corresponding Source. The information must suffice to ensure that the continued functioning of the modified object code is in no case prevented or interfered with solely because modification has been made. If you convey an object code work under this section in, or with, or specifically for use in, a User Product, and the conveying occurs as part of a transaction in which the right of possession and use of the User Product is transferred to the recipient in perpetuity or for a fixed term (regardless of how the transaction is characterized), the Corresponding Source conveyed under this section must be accompanied by the Installation Information. But this requirement does not apply if neither you nor any third party retains the ability to install modified object code on the User Product (for example, the work has been installed in ROM). The requirement to provide Installation Information does not include a requirement to continue to provide support service, warranty, or updates for a work that has been modified or installed by the recipient, or for the User Product in which it has been modified or installed. Access to a network may be denied when the modification itself materially and adversely affects the operation of the network or violates the rules and protocols for communication across the network. Corresponding Source conveyed, and Installation Information provided, in accord with this section must be in a format that is publicly documented (and with an implementation available to the public in source code form), and must require no special password or key for unpacking, reading or copying. 7. Additional Terms. "Additional permissions" are terms that supplement the terms of this License by making exceptions from one or more of its conditions. Additional permissions that are applicable to the entire Program shall be treated as though they were included in this License, to the extent that they are valid under applicable law. If additional permissions apply only to part of the Program, that part may be used separately under those permissions, but the entire Program remains governed by this License without regard to the additional permissions. When you convey a copy of a covered work, you may at your option remove any additional permissions from that copy, or from any part of it. (Additional permissions may be written to require their own removal in certain cases when you modify the work.) You may place additional permissions on material, added by you to a covered work, for which you have or can give appropriate copyright permission. Notwithstanding any other provision of this License, for material you add to a covered work, you may (if authorized by the copyright holders of that material) supplement the terms of this License with terms: a) Disclaiming warranty or limiting liability differently from the terms of sections 15 and 16 of this License; or b) Requiring preservation of specified reasonable legal notices or author attributions in that material or in the Appropriate Legal Notices displayed by works containing it; or c) Prohibiting misrepresentation of the origin of that material, or requiring that modified versions of such material be marked in reasonable ways as different from the original version; or d) Limiting the use for publicity purposes of names of licensors or authors of the material; or e) Declining to grant rights under trademark law for use of some trade names, trademarks, or service marks; or f) Requiring indemnification of licensors and authors of that material by anyone who conveys the material (or modified versions of it) with contractual assumptions of liability to the recipient, for any liability that these contractual assumptions directly impose on those licensors and authors. All other non-permissive additional terms are considered "further restrictions" within the meaning of section 10. If the Program as you received it, or any part of it, contains a notice stating that it is governed by this License along with a term that is a further restriction, you may remove that term. If a license document contains a further restriction but permits relicensing or conveying under this License, you may add to a covered work material governed by the terms of that license document, provided that the further restriction does not survive such relicensing or conveying. If you add terms to a covered work in accord with this section, you must place, in the relevant source files, a statement of the additional terms that apply to those files, or a notice indicating where to find the applicable terms. Additional terms, permissive or non-permissive, may be stated in the form of a separately written license, or stated as exceptions; the above requirements apply either way. 8. Termination. You may not propagate or modify a covered work except as expressly provided under this License. Any attempt otherwise to propagate or modify it is void, and will automatically terminate your rights under this License (including any patent licenses granted under the third paragraph of section 11). However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, you do not qualify to receive new licenses for the same material under section 10. 9. Acceptance Not Required for Having Copies. You are not required to accept this License in order to receive or run a copy of the Program. Ancillary propagation of a covered work occurring solely as a consequence of using peer-to-peer transmission to receive a copy likewise does not require acceptance. However, nothing other than this License grants you permission to propagate or modify any covered work. These actions infringe copyright if you do not accept this License. Therefore, by modifying or propagating a covered work, you indicate your acceptance of this License to do so. 10. Automatic Licensing of Downstream Recipients. Each time you convey a covered work, the recipient automatically receives a license from the original licensors, to run, modify and propagate that work, subject to this License. You are not responsible for enforcing compliance by third parties with this License. An "entity transaction" is a transaction transferring control of an organization, or substantially all assets of one, or subdividing an organization, or merging organizations. If propagation of a covered work results from an entity transaction, each party to that transaction who receives a copy of the work also receives whatever licenses to the work the party's predecessor in interest had or could give under the previous paragraph, plus a right to possession of the Corresponding Source of the work from the predecessor in interest, if the predecessor has it or can get it with reasonable efforts. You may not impose any further restrictions on the exercise of the rights granted or affirmed under this License. For example, you may not impose a license fee, royalty, or other charge for exercise of rights granted under this License, and you may not initiate litigation (including a cross-claim or counterclaim in a lawsuit) alleging that any patent claim is infringed by making, using, selling, offering for sale, or importing the Program or any portion of it. 11. Patents. A "contributor" is a copyright holder who authorizes use under this License of the Program or a work on which the Program is based. The work thus licensed is called the contributor's "contributor version". A contributor's "essential patent claims" are all patent claims owned or controlled by the contributor, whether already acquired or hereafter acquired, that would be infringed by some manner, permitted by this License, of making, using, or selling its contributor version, but do not include claims that would be infringed only as a consequence of further modification of the contributor version. For purposes of this definition, "control" includes the right to grant patent sublicenses in a manner consistent with the requirements of this License. Each contributor grants you a non-exclusive, worldwide, royalty-free patent license under the contributor's essential patent claims, to make, use, sell, offer for sale, import and otherwise run, modify and propagate the contents of its contributor version. In the following three paragraphs, a "patent license" is any express agreement or commitment, however denominated, not to enforce a patent (such as an express permission to practice a patent or covenant not to sue for patent infringement). To "grant" such a patent license to a party means to make such an agreement or commitment not to enforce a patent against the party. If you convey a covered work, knowingly relying on a patent license, and the Corresponding Source of the work is not available for anyone to copy, free of charge and under the terms of this License, through a publicly available network server or other readily accessible means, then you must either (1) cause the Corresponding Source to be so available, or (2) arrange to deprive yourself of the benefit of the patent license for this particular work, or (3) arrange, in a manner consistent with the requirements of this License, to extend the patent license to downstream recipients. "Knowingly relying" means you have actual knowledge that, but for the patent license, your conveying the covered work in a country, or your recipient's use of the covered work in a country, would infringe one or more identifiable patents in that country that you have reason to believe are valid. If, pursuant to or in connection with a single transaction or arrangement, you convey, or propagate by procuring conveyance of, a covered work, and grant a patent license to some of the parties receiving the covered work authorizing them to use, propagate, modify or convey a specific copy of the covered work, then the patent license you grant is automatically extended to all recipients of the covered work and works based on it. A patent license is "discriminatory" if it does not include within the scope of its coverage, prohibits the exercise of, or is conditioned on the non-exercise of one or more of the rights that are specifically granted under this License. You may not convey a covered work if you are a party to an arrangement with a third party that is in the business of distributing software, under which you make payment to the third party based on the extent of your activity of conveying the work, and under which the third party grants, to any of the parties who would receive the covered work from you, a discriminatory patent license (a) in connection with copies of the covered work conveyed by you (or copies made from those copies), or (b) primarily for and in connection with specific products or compilations that contain the covered work, unless you entered into that arrangement, or that patent license was granted, prior to 28 March 2007. Nothing in this License shall be construed as excluding or limiting any implied license or other defenses to infringement that may otherwise be available to you under applicable patent law. 12. No Surrender of Others' Freedom. If conditions are imposed on you (whether by court order, agreement or otherwise) that contradict the conditions of this License, they do not excuse you from the conditions of this License. If you cannot convey a covered work so as to satisfy simultaneously your obligations under this License and any other pertinent obligations, then as a consequence you may not convey it at all. For example, if you agree to terms that obligate you to collect a royalty for further conveying from those to whom you convey the Program, the only way you could satisfy both those terms and this License would be to refrain entirely from conveying the Program. 13. Use with the GNU Affero General Public License. Notwithstanding any other provision of this License, you have permission to link or combine any covered work with a work licensed under version 3 of the GNU Affero General Public License into a single combined work, and to convey the resulting work. The terms of this License will continue to apply to the part which is the covered work, but the special requirements of the GNU Affero General Public License, section 13, concerning interaction through a network will apply to the combination as such. 14. Revised Versions of this License. The Free Software Foundation may publish revised and/or new versions of the GNU General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Program specifies that a certain numbered version of the GNU General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that numbered version or of any later version published by the Free Software Foundation. If the Program does not specify a version number of the GNU General Public License, you may choose any version ever published by the Free Software Foundation. If the Program specifies that a proxy can decide which future versions of the GNU General Public License can be used, that proxy's public statement of acceptance of a version permanently authorizes you to choose that version for the Program. Later license versions may give you additional or different permissions. However, no additional obligations are imposed on any author or copyright holder as a result of your choosing to follow a later version. 15. Disclaimer of Warranty. THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 16. Limitation of Liability. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. 17. Interpretation of Sections 15 and 16. If the disclaimer of warranty and limitation of liability provided above cannot be given local legal effect according to their terms, reviewing courts shall apply local law that most closely approximates an absolute waiver of all civil liability in connection with the Program, unless a warranty or assumption of liability accompanies a copy of the Program in return for a fee. END OF TERMS AND CONDITIONS How to Apply These Terms to Your New Programs If you develop a new program, and you want it to be of the greatest possible use to the public, the best way to achieve this is to make it free software which everyone can redistribute and change under these terms. To do so, attach the following notices to the program. It is safest to attach them to the start of each source file to most effectively state the exclusion of warranty; and each file should have at least the "copyright" line and a pointer to where the full notice is found. Copyright (C) This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 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 General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . Also add information on how to contact you by electronic and paper mail. If the program does terminal interaction, make it output a short notice like this when it starts in an interactive mode: Copyright (C) This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. This is free software, and you are welcome to redistribute it under certain conditions; type `show c' for details. The hypothetical commands `show w' and `show c' should show the appropriate parts of the General Public License. Of course, your program's commands might be different; for a GUI interface, you would use an "about box". You should also get your employer (if you work as a programmer) or school, if any, to sign a "copyright disclaimer" for the program, if necessary. For more information on this, and how to apply and follow the GNU GPL, see . The GNU General Public License does not permit incorporating your program into proprietary programs. If your program is a subroutine library, you may consider it more useful to permit linking proprietary applications with the library. If this is what you want to do, use the GNU Lesser General Public License instead of this License. But first, please read . django-cas-server-1.1.0/setup.cfg0000644000175000017500000000023313436457571020343 0ustar valentinvalentin00000000000000[metadata] description-file = README.rst [egg_info] tag_build = tag_date = 0 tag_svn_revision = 0 [aliases] test = pytest [bdist_wheel] universal = 1 django-cas-server-1.1.0/CHANGELOG.rst0000644000175000017500000003706313436457341020551 0ustar valentinvalentin00000000000000Change Log ########## All notable changes to this project will be documented in this file. .. contents:: Table of Contents :depth: 2 v1.1.0 - 2019-03-02 =================== Added ----- * Support for Django 2.1 Fixes ----- * Checkbox position on the login page * Set ldap3 client_strategy from sync to sync-restartable * Deprecation warning for {% load staticfiles %} and django.contrib.staticfiles v1.0.0 - 2019-01-12 =================== Added ----- * Support for python 3.6 and Django 1.11 * Support for Django 2.0 * Keep query string then redirecting from / to /login Fixes ----- * Add missing attributes authenticationDate, longTermAuthenticationRequestTokenUsed and isFromNewLogin from service validation response * Catch error from calling django.contrib.staticfiles.templatetags.staticfiles.static in non-debug mode before collectstatic in cas_server.default_settings.py * Invalid escape sequence in regular expression Deprecated ---------- * Support for Django <1.11 is dropped, it should still works for this version. Next versions will most probably be not compatible with Django <1.11 * Support for python 3.4 is dropped, it should still works for this version. Next versions may or may not works with python 3.4. Other ----- * Migrations have been squashed for Django 2.0 support. Be sur to apply all migration before updating to this version * Update PyPi url from https://pypi.python.org to https://pypi.org v0.9.0 - 2017-11-17 =================== Added ----- * Dutch translation * Protuguese translation (brazilian variant) * Support for ldap3 version 2 or more (changes in the API) All exception are now in ldap3.core.exceptions, methodes for fetching attritutes and dn are renamed. * Possibility to disable service message boxes on the login pages Fixed ----- * Then using the LDAP auth backend with ``bind`` method for password check, do not try to bind if the user dn was not found. This was causing the exception ``'NoneType' object has no attribute 'getitem'`` describe in #21 * Increase the max size of usernames (30 chars to 250) * Fix XSS js injection v0.8.0 - 2017-03-08 =================== Added ----- * Add a test for login with missing parameter (username or password or both) * Add ldap auth using bind method (use the user credentials to bind the the ldap server and let the server check the credentials) * Add CAS_TGT_VALIDITY parameter: Max time after with the user MUST reauthenticate. Fixed ----- * Allow both unicode and bytes dotted string in utils.import_attr * Fix some spelling and grammar on log messages. (thanks to Allie Micka) * Fix froms css class error on success/error due to a scpaless block * Disable pip cache then installing with make install Changed ------- * Update french translation v0.7.4 - 2016-09-07 =================== Fixed ----- * Add templatetags to Pypi package v0.7.3 - 2016-09-07 =================== Added ----- * Add autofocus to the username input on the login page Fixed ----- * Really pick the last version on Pypi for new version checking. We were only sorting version string lexicographically and it would have break when we reach version 0.10.N or 0.N.10 * Only check for valid username/password if username and password POST fields are posted. This fix a bug where posting without it raise a exception are None where passed for username/password verification. v0.7.2 - 2016-08-31 =================== Added ----- * Add Django 1.10 support * Add support of gitlab continuous integration Fixed ----- * Fix BootsrapForm: placeholder on Input and Textarea only, use class form-control on Input, Select and Textarea. * Fix lang attribute in django 1.7. On html pages, the lang attribute of the was not present in django 1.7. We use now a methode to display it that is also available in django 1.7 v0.7.1 - 2016-08-24 =================== Added ----- * Add a forgotten migration (only change help_text and validators) v0.7.0 - 2016-08-24 =================== Added ----- * Add a CHANGELOG.rst file. * Add a validator to models CharField that should be regular expressions checking that user input are valids regular expressions. * Add a CAS_INFO_MESSAGES and CAS_INFO_MESSAGES_ORDER settings allowing to display messages in info-boxes on the html pages of the default templates. Changed ------- * Allow the user defined CAS_COMPONENT_URLS to omit not changed values. * replace code-block without language indication by literal blocks. * Update french translation Fixed ----- * Some README.rst typos. * some english typos v0.6.4 - 2016-08-14 =================== commit: 282e3a831b3c0b0818881c2f16d056850d572b89 Added ----- * Add a forgotten migration (only change help_text) v0.6.3 - 2016-08-14 =================== commit: 07a537b403c5c5e39a4ddd084f90e3a4de88a54e Added ----- * Add powered by footer * Add a github version badge * documents templatetags Changed ------- * Usage of the documented API for models _meta in auth.DjangoAuthUser * set warn cookie using javascript if possible * Unfold many to many attributes in auth.DjangoAuthUser attributes Fixed ----- * typos in README.rst * w3c validation Cleaned ------- * Code factorisation (models.py, views.py) v0.6.2 - 2016-08-02 =================== commit: 773707e6c3c3fa20f697c946e31cafc591e8fee8 Added ----- * Support authentication renewal in federate mode * Add new version email and info box then new version is available * Add SqlAuthUser and LdapAuthUser auth classes. Deprecate the usage of MysqlAuthUser in favor of SqlAuthUser. * Add pytest-warning to tests * Add a checkbox to forget the identity provider if we checked "remember the identity provider" * Add dependancies correspondance between python pypi, debian and centos packages in README Changed ------- * Move coverage computation last in travis * Enable logging to stderr then running tests * Remember "warn me before…" using a cookie * Put favicon (shortcut icon) URL in settings Deprecated ---------- * The auth class MysqlAuthUser is deprecated in favor of the SqlAuthUser class. Fixed ----- * Use custom templatetags instead settings custom attributes to Boundfields (As it do not work with django 1.7) * Display an error message on bad response from identity provider in federate mode instead of crashing. (e.g. Bad XML document) * Catch base64 decode error on b64decode to raise our custom exception BadHash * Add secret as sensitive variables/post parameter for /auth * Only set "remember my provider" in federated mode upon successful authentication * Since we drop django-boostrap3 dependancies, Django default minimal version is 1.7.1 * [cas.py] Append renew=true when validating tickets Cleaned ------- * code factorization (cas.py, forms.py) v0.6.1 - 2016-07-27 =================== commit: b168e0a6423c53de31aae6c444fa1d1c5083afa6 Added ----- * Add sphinx docs + autodoc * Add the possibility to run tests with "setup.py test" * Include docs, Makefile, coverage config and tests config to source package * Add serviceValidate ProxyTicket tests * Add python 3.5 tox/travis tests Changed ------- * Use https://badges.genua.fr for badges Fixed ----- * Keep LoginTicket list upon fail authentication (It prevent the next login attemps to fail because of bad LT) Cleaned ------- * Compact federated mode migration * Reformat default_settings.py for documentation using sphinx autodoc * Factorize some code (from views.py to Ticket models class methods) * Update urlpattern for django 1.10 * Drop dependancies django-picklefield and django-bootstrap3 v0.6.0 - 2016-07-06 =================== commit: 4ad4d13baa4236c5cd72cc5216d7ff08dd361476 Added ----- * Add a section describing service patterns options to README.rst * Add a federation mode: When the settings CAS_FEDERATE is True, django-cas-server will offer to the user to choose its CAS backend to authenticate. Hence the login page do not display anymore a username/password form but a select form with configured CASs backend. This allow to give access to CAS supported applications to users from multiple organization seamlessly. It was originally developped to mach the need of https://ares.fr (Federated CAS at https://cas.ares.fr, example of an application using it as https://chat.myares.fr) Fixed ----- * Then a ticket was marked as obtained with the user entering its credentials (aka not by SSO), and the service did not require it, ticket validation was failing. Now, if the service do not require authentication to be renewed, both ticket with renewed authentication and non renewed authentication validate successfully. v0.5.0 - 2016-07-01 =================== commit: e3ab64271b718a17e4cbbbabda0a2453107a83df Added ----- * Add more password scheme support to the mysql authentication backend: ldap user attribute scheme encoding and simple password hash in hexa for md5, sha1, sha224, sha256, sha384, sha512. * Add a main heading to template "Central Authentication Service" with a logo controled by CAS_LOGO_URL * Add logos to the project (svg, png) * Add coverage computation * link project to codacy * Update doc: add debian requirement, correct typos, correct links Changed ------- * Use settings to set tests username password and attributes * Tweak the css and html for small screens * Update travis cache for faster build * clean Makefile, use pip to install, add target for tests Fixed ----- * Fix "warn me": we generate the ticket after the user agree to be connected to the service. we were generating first and the connect button was a link to the service url with the ?ticket= this could lead to situation where the ticket validity expire if the user is slow to click the connect button. * Fix authentication renewal: the renew parameter were not transmited when POST the login request and self.renew (aks for auth renewal) was use instead of self.renewed (auth was renewd) when generating a ticket. * Fix attribute value replacement when generating a ticket: we were using the 'name' attribute instead of the 'attribut' attribut on ReplaceAttributValue * Fix attribute value replacement when generating a ticket then the value is a list: iterate over each element of the list. * Fix a NameError in utils.import_attr * Fix serviceValidate and samlValidate when user_field is an attribute that is a list: we use the first element of the list as username. we were serializing the list before that. * Correct typos Cleaned ------- * Clean some useless conditional branches found with coverage * Clean cas.js: use compact object declararion * Use six for python{2|3} compatibility * Move all unit tests to cas_server.tests and use django primitive. We also have a 100% tests coverage now. Using the django classes for tests, we do not need to use our own dirty mock. * Move mysql backend password check to a function in utils v0.4.4 - 2016-04-30 =================== commit: 77d1607b0beefe8b171adcd8e2dcd974e3cdc72a Added ----- * Add sensitive_post_parameters and sensitive_variables for passwords, so passwords are anonymised before django send an error report. Fixed ----- * Before commit 77fc5b5 the User model had a foreign key to the Session model. After the commit, Only the session_key is store, allowing to use different backend than the Session SQL backend. So the first migration (which is 21 migrations combined) was creating the User model with the foreign key, then delete it and add the field session_key. Somehow, MySQL did not like it. Now the first migration directly create the User model with the session_key and without the foreign key to the Session SQL backend. * Evaluate attributes variables in the template samlValidate.xml. the {{ }} was missing causing the variable name to be displyed instead of the variable content. * Return username in CAS 1.0 on the second ligne of the CAS response as specified. Changed ------- * Update tests v0.4.3 - 2016-03-18 =================== commit: f6d436acb49f8d32b5457c316c18c4892accfd3b Fixed ----- * Currently, one of our dependancy, django-boostrap3, do not support django 1.7 in its last version. So there is some detection of the current django installed version in setup.py to pin django-boostrap3 to a version supported by django 1.7 if django 1.7 is installed, or to require at least django 1.8. The detection did not handle the case where django was not installed. * [PEP8] Put line breaks after binary operator and not before. v0.4.2 - 2016-03-18 =================== commit: d1cd17d6103281b03a8c57013671057eab80d21c Added ----- * On logout, display the number of sessions we are logged out from. Fixed ----- * One of our dependancy, django-boostrap3, do not support django 1.7 in its last version. Some django version detection is added to setup.py to handle that. * Some typos * Make errors returned by utils.import_attr clearer (as they are likely to be displayed to the django admin) v0.4.1 - 2015-12-23 =================== commit: 5e63f39f9b7c678a300ad2f8132166be34d1d35b Added ----- * Add a run_test_server target to make file. Running make run_test_server will build a virtualenv, create a django projet with django-cas-server and lauch ./management.py runserver. It is quite handy to test developement version. * Add verbose name for cas_server app and models * Add Makefile clean targets for tox tests and test virtualenv. * Add link on license badge to the GPLv3 Changed ------- * Make Makefile clean targets modular * Use img.shields.io for PyPi badges * Get django-cas-server version in Makefile directly from setup.py (so now, the version is only written in one place) Fixed ----- * Fix MysqlAuthUser when number of results != 1: In that case, call super anyway this the provided username. v0.4.0 - 2015-12-15 =================== commit: 7b4fac575449e50c2caff07f5798dba7f4e4857c Added ----- * Add a help_text to pattern of ServicePattern * Add a timeout to SLO requests * Add logging capabilities (see README.rst for instruction) * Add management commands that should be called on a regular basis to README.rst v0.3.5 - 2015-12-12 =================== commit: 51fa0861f550723171e52d58025fa789dccb8cde Added ----- * Add badges to README.rst * Document settings parameter in README.rst * Add a "Features" section in README.rst Changed ------- * Add a AuthUser auth class and use it as auth classes base class instead of DummyAuthUser Fixed ----- * Fix minor errors and typos in README.rst v0.3.4 - 2015-12-12 =================== commit: 9fbfe19c550b147e8d0377108cdac8231cf0fb27 Added ----- * Add static files, templates and locales to the PyPi release by adding them to MANIFEST.in * Add a Makefile with the build/install/clean/dist targets v0.3.3 - 2015-12-12 =================== commit: 16b700d0127abe33a1eabf5d5fe890aeb5167e5a Added ----- * Add management commands and migrations to the package by adding there packages to setup.py packages list. v0.3.2 - 2015-12-12 [YANKED] ============================ commit: eef9490885bf665a53349573ddb9cbe844319b3e Added ----- * Add migrations to setup.py package_data v0.3.1 - 2015-12-12 =================== commit: d0f6ed9ea3a4b3e2bf715fd218c460892c32e39f Added ----- * Add a forgotten migration (remove auto_now_add=True from the User model) v0.3.0 - 2015-12-12 =================== commit: b69769d71a99806a69e300eca0d7c6744a2b327e Added ----- * Django 1.9 compatibility (add tox and travis tests and fix some decrecated) v0.2.1 - 2015-12-12 =================== commit: 90e077dedb991d651822e9bb283470de8bddd7dd First github and PyPi release Fixed ----- * Prune .tox in MANIFEST.in * add dist/ to .gitignore * typo in setup.cfg v0.2.0 - 2015-12-12 [YANKED] ============================ commit: a071ad46d7cd76fc97eb86f2f538d330457c6767 v0.1.0 - 2015-05-22 [YANKED] ============================ commit: 6981433bdf8a406992ba0c5e844a47d06ccc08fb django-cas-server-1.1.0/README.rst0000644000175000017500000007656613416345433020226 0ustar valentinvalentin00000000000000CAS Server ########## |travis| |coverage| |licence| |github_version| |pypi_version| |codacy| |doc| CAS Server is a Django application implementing the `CAS Protocol 3.0 Specification `_. By default, the authentication process use django internal users but you can easily use any sources (see the `Authentication backend`_ section and auth classes in the auth.py file) .. contents:: Table of Contents Features ======== * Support CAS version 1.0, 2.0, 3.0 * Support Single Sign Out * Configuration of services via the django Admin application * Fine control on which user's attributes are passed to which service * Possibility to rename/rewrite attributes per service * Possibility to require some attribute values per service * Federated mode between multiple CAS * Supports Django 1.11 and 2.0 * Supports Python 2.7, 3.5+ Dependencies ============ ``django-cas-server`` depends on the following python packages: * Django >= 1.11 < 2.1 * requests >= 2.4 * requests_futures >= 0.9.5 * lxml >= 3.4 * six >= 1.8 Minimal version of packages dependancy are just indicative and meens that ``django-cas-server`` has been tested with it. Previous versions of dependencies may or may not work. Additionally, denpending of the `Authentication backend`_ you plan to use, you may need the following python packages: * ldap3 * psycopg2 * mysql-python Here there is a table with the name of python packages and the corresponding packages providing them on debian like systems and centos like systems. You should try as much as possible to use system packages as there are automatically updated then you update your system. You can then install Not Available (N/A) packages on your system using pip inside a virtualenv as described in the `Installation`_ section. For use with python3, just replace python(2) in the table by python3. +------------------+-------------------------+---------------------+ | python package | debian like systems | centos like systems | +==================+=========================+=====================+ | Django | python-django | python-django | +------------------+-------------------------+---------------------+ | requests | python-requests | python-requests | +------------------+-------------------------+---------------------+ | requests_futures | python-requests-futures | N/A | +------------------+-------------------------+---------------------+ | lxml | python-lxml | python-lxml | +------------------+-------------------------+---------------------+ | six | python-six | python-six | +------------------+-------------------------+---------------------+ | ldap3 | python-ldap3 | python-ldap3 | +------------------+-------------------------+---------------------+ | psycopg2 | python-psycopg2 | python-psycopg2 | +------------------+-------------------------+---------------------+ | mysql-python | python-mysqldb | python2-mysql | +------------------+-------------------------+---------------------+ Installation ============ The recommended installation mode is to use a virtualenv with ``--system-site-packages`` 1. Make sure that python virtualenv is installed 2. Install python packages available via the system package manager: On debian like systems:: $ sudo apt-get install python-django python-requests python-six python-lxml python-requests-futures On debian jessie, you can use the version of python-django available in the `backports `_. On centos like systems:: $ sudo yum install python-django python-requests python-six python-lxml 3. Create a virtualenv:: $ virtualenv --system-site-packages cas_venv Running virtualenv with interpreter /var/www/html/cas-server/bin/python2 Using real prefix '/usr' New python executable in cas/bin/python2 Also creating executable in cas/bin/python Installing setuptools, pip...done. 4. And `activate it `__:: $ cd cas_venv/; . bin/activate 5. Create a django project:: $ django-admin startproject cas_project $ cd cas_project 6. Install `django-cas-server`. To use the last published release, run:: $ pip install django-cas-server Alternatively if you want to use the version of the git repository, you can clone it:: $ git clone https://github.com/nitmir/django-cas-server $ cd django-cas-server $ pip install -r requirements.txt Then, either run ``make install`` to create a python package using the sources of the repository and install it with pip, or place the ``cas_server`` directory into your `PYTHONPATH `_ (for instance by symlinking ``cas_server`` to the root of your django project). 7. Open ``cas_project/settings.py`` in you favourite editor and follow the quick start section. Quick start =========== 1. Add "cas_server" to your INSTALLED_APPS setting like this:: INSTALLED_APPS = ( 'django.contrib.admin', ... 'cas_server', ) For internationalization support, add "django.middleware.locale.LocaleMiddleware" to your MIDDLEWARE_CLASSES setting like this:: MIDDLEWARE_CLASSES = ( ... 'django.middleware.locale.LocaleMiddleware', ... ) 2. Include the cas_server URLconf in your project urls.py like this:: from django.conf.urls import url, include urlpatterns = [ url(r'^admin/', admin.site.urls), ... url(r'^cas/', include('cas_server.urls', namespace="cas_server")), ] 3. Run ``python manage.py migrate`` to create the cas_server models. 4. You should add some management commands to a crontab: ``clearsessions``, ``cas_clean_tickets`` and ``cas_clean_sessions``. * ``clearsessions``: please see `Clearing the session store `_. * ``cas_clean_tickets``: old tickets and timed-out tickets do not get purge from the database automatically. They are just marked as invalid. ``cas_clean_tickets`` is a clean-up management command for this purpose. It send SingleLogOut request to services with timed out tickets and delete them. * ``cas_clean_sessions``: Logout and purge users (sending SLO requests) that are inactive since more than ``SESSION_COOKIE_AGE``. The default value for is ``1209600`` seconds (2 weeks). You probably should reduce it to something like ``86400`` seconds (1 day). You could for example do as bellow:: 0 0 * * * cas-user /path/to/project/manage.py clearsessions */5 * * * * cas-user /path/to/project/manage.py cas_clean_tickets 5 0 * * * cas-user /path/to/project/manage.py cas_clean_sessions 5. Run ``python manage.py createsuperuser`` to create an administrator user. 6. Start the development server and visit http://127.0.0.1:8000/admin/ to add a first service allowed to authenticate user against the CAS (you'll need the Admin app enabled). See the `Service Patterns`_ section bellow. 7. Visit http://127.0.0.1:8000/cas/ to login with your django users. Settings ======== All settings are optional. Add them to ``settings.py`` to customize ``django-cas-server``: Template settings ----------------- * ``CAS_LOGO_URL``: URL to the logo showed in the up left corner on the default templates. Set it to ``False`` to disable it. * ``CAS_FAVICON_URL``: URL to the favicon (shortcut icon) used by the default templates. Default is a key icon. Set it to ``False`` to disable it. * ``CAS_SHOW_POWERED``: Set it to ``False`` to hide the powered by footer. The default is ``True``. * ``CAS_COMPONENT_URLS``: URLs to css and javascript external components. It is a dictionnary having the five following keys: ``"bootstrap3_css"``, ``"bootstrap3_js"``, ``"html5shiv"``, ``"respond"``, ``"jquery"``. The default is:: { "bootstrap3_css": "//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css", "bootstrap3_js": "//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js", "html5shiv": "//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js", "respond": "//oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js", "jquery": "//code.jquery.com/jquery.min.js", } if you omit some keys of the dictionnary, the default value for these keys is used. * ``CAS_SHOW_SERVICE_MESSAGES``: Messages displayed about the state of the service on the login page. The default is ``True``. * ``CAS_INFO_MESSAGES``: Messages displayed in info-boxes on the html pages of the default templates. It is a dictionnary mapping message name to a message dict. A message dict has 3 keys: * ``message``: A unicode message to display, potentially wrapped around ugettex_lazy * ``discardable``: A boolean, specify if the users can close the message info-box * ``type``: One of info, success, info, warning, danger. The type of the info-box. ``CAS_INFO_MESSAGES`` contains by default one message, ``cas_explained``, which explain roughly the purpose of a CAS. The default is:: { "cas_explained": { "message":_( u"The Central Authentication Service grants you access to most of our websites by " u"authenticating only once, so you don't need to type your credentials again unless " u"your session expires or you logout." ), "discardable": True, "type": "info", # one of info, success, info, warning, danger }, } * ``CAS_INFO_MESSAGES_ORDER``: A list of message names. Order in which info-box messages are displayed. Use an empty list to disable messages display. The default is ``[]``. * ``CAS_LOGIN_TEMPLATE``: Path to the template showed on ``/login`` then the user is not autenticated. The default is ``"cas_server/login.html"``. * ``CAS_WARN_TEMPLATE``: Path to the template showed on ``/login?service=...`` then the user is authenticated and has asked to be warned before being connected to a service. The default is ``"cas_server/warn.html"``. * ``CAS_LOGGED_TEMPLATE``: Path to the template showed on ``/login`` then to user is authenticated. The default is ``"cas_server/logged.html"``. * ``CAS_LOGOUT_TEMPLATE``: Path to the template showed on ``/logout`` then to user is being disconnected. The default is ``"cas_server/logout.html"`` * ``CAS_REDIRECT_TO_LOGIN_AFTER_LOGOUT``: Should we redirect users to ``/login`` after they logged out instead of displaying ``CAS_LOGOUT_TEMPLATE``. The default is ``False``. Authentication settings ----------------------- * ``CAS_AUTH_CLASS``: A dotted path to a class or a class implementing ``cas_server.auth.AuthUser``. The default is ``"cas_server.auth.DjangoAuthUser"`` Available classes bundled with ``django-cas-server`` are listed below in the `Authentication backend`_ section. * ``SESSION_COOKIE_AGE``: This is a django settings. Here, it control the delay in seconds after which inactive users are logged out. The default is ``1209600`` (2 weeks). You probably should reduce it to something like ``86400`` seconds (1 day). * ``CAS_TGT_VALIDITY``: Max time after with the user MUST reauthenticate. Let it to `None` for no max time.This can be used to force refreshing cached informations only available upon user authentication like the user attributes in federation mode or with the ldap auth in bind mode. The default is ``None``. * ``CAS_PROXY_CA_CERTIFICATE_PATH``: Path to certificate authorities file. Usually on linux the local CAs are in ``/etc/ssl/certs/ca-certificates.crt``. The default is ``True`` which tell requests to use its internal certificat authorities. Settings it to ``False`` should disable all x509 certificates validation and MUST not be done in production. x509 certificate validation is perform upon PGT issuance. * ``CAS_SLO_MAX_PARALLEL_REQUESTS``: Maximum number of parallel single log out requests send. If more requests need to be send, there are queued. The default is ``10``. * ``CAS_SLO_TIMEOUT``: Timeout for a single SLO request in seconds. The default is ``5``. Federation settings ------------------- * ``CAS_FEDERATE``: A boolean for activating the federated mode (see the `Federation mode`_ section below). The default is ``False``. * ``CAS_FEDERATE_REMEMBER_TIMEOUT``: Time after witch the cookie use for "remember my identity provider" expire. The default is ``604800``, one week. The cookie is called ``_remember_provider``. New version warnings settings ----------------------------- * ``CAS_NEW_VERSION_HTML_WARNING``: A boolean for diplaying a warning on html pages then a new version of the application is avaible. Once closed by a user, it is not displayed to this user until the next new version. The default is ``True``. * ``CAS_NEW_VERSION_EMAIL_WARNING``: A boolean for sending a email to ``settings.ADMINS`` when a new version is available. The default is ``True``. Tickets validity settings ------------------------- * ``CAS_TICKET_VALIDITY``: Number of seconds the service tickets and proxy tickets are valid. This is the maximal time between ticket issuance by the CAS and ticket validation by an application. The default is ``60``. * ``CAS_PGT_VALIDITY``: Number of seconds the proxy granting tickets are valid. The default is ``3600`` (1 hour). * ``CAS_TICKET_TIMEOUT``: Number of seconds a ticket is kept in the database before sending Single Log Out request and being cleared. The default is ``86400`` (24 hours). Tickets miscellaneous settings ------------------------------ * ``CAS_TICKET_LEN``: Default ticket length. All CAS implementation MUST support ST and PT up to 32 chars, PGT and PGTIOU up to 64 chars and it is RECOMMENDED that all tickets up to 256 chars are supports. Here the default is ``64``. * ``CAS_LT_LEN``: Length of the login tickets. Login tickets are only processed by ``django-cas-server`` thus there is no length restriction on it. The default is ``CAS_TICKET_LEN``. * ``CAS_ST_LEN``: Length of the service tickets. The default is ``CAS_TICKET_LEN``. You may need to lower is to ``32`` if you use some old clients. * ``CAS_PT_LEN``: Length of the proxy tickets. The default is ``CAS_TICKET_LEN``. This length should be the same as ``CAS_ST_LEN``. You may need to lower is to ``32`` if you use some old clients. * ``CAS_PGT_LEN``: Length of the proxy granting tickets. The default is ``CAS_TICKET_LEN``. * ``CAS_PGTIOU_LEN``: Length of the proxy granting tickets IOU. The default is ``CAS_TICKET_LEN``. * ``CAS_LOGIN_TICKET_PREFIX``: Prefix of login tickets. The default is ``"LT"``. * ``CAS_SERVICE_TICKET_PREFIX``: Prefix of service tickets. The default is ``"ST"``. The CAS specification mandate that service tickets MUST begin with the characters ST so you should not change this. * ``CAS_PROXY_TICKET_PREFIX``: Prefix of proxy ticket. The default is ``"PT"``. * ``CAS_PROXY_GRANTING_TICKET_PREFIX``: Prefix of proxy granting ticket. The default is ``"PGT"``. * ``CAS_PROXY_GRANTING_TICKET_IOU_PREFIX``: Prefix of proxy granting ticket IOU. The default is ``"PGTIOU"``. Mysql backend settings ---------------------- Deprecated, see the `Sql backend settings`_. Only usefull if you are using the mysql authentication backend: * ``CAS_SQL_HOST``: Host for the SQL server. The default is ``"localhost"``. * ``CAS_SQL_USERNAME``: Username for connecting to the SQL server. * ``CAS_SQL_PASSWORD``: Password for connecting to the SQL server. * ``CAS_SQL_DBNAME``: Database name. * ``CAS_SQL_DBCHARSET``: Database charset. The default is ``"utf8"`` * ``CAS_SQL_USER_QUERY``: The query performed upon user authentication. The username must be in field ``username``, the password in ``password``, additional fields are used as the user attributes. The default is ``"SELECT user AS username, pass AS password, users.* FROM users WHERE user = %s"`` * ``CAS_SQL_PASSWORD_CHECK``: The method used to check the user password. Must be one of the following: * ``"crypt"`` (see ), the password in the database should begin this $ * ``"ldap"`` (see https://tools.ietf.org/id/draft-stroeder-hashed-userpassword-values-01.html) the password in the database must begin with one of {MD5}, {SMD5}, {SHA}, {SSHA}, {SHA256}, {SSHA256}, {SHA384}, {SSHA384}, {SHA512}, {SSHA512}, {CRYPT}. * ``"hex_HASH_NAME"`` with ``HASH_NAME`` in md5, sha1, sha224, sha256, sha384, sha512. The hashed password in the database is compare to the hexadecimal digest of the clear password hashed with the corresponding algorithm. * ``"plain"``, the password in the database must be in clear. The default is ``"crypt"``. Sql backend settings -------------------- Only usefull if you are using the sql authentication backend. You must add a ``"cas_server"`` database to `settings.DATABASES `__ as defined in the django documentation. It is then the database use by the sql backend. * ``CAS_SQL_USER_QUERY``: The query performed upon user authentication. The username must be in field ``username``, the password in ``password``, additional fields are used as the user attributes. The default is ``"SELECT user AS username, pass AS password, users.* FROM users WHERE user = %s"`` * ``CAS_SQL_PASSWORD_CHECK``: The method used to check the user password. Must be one of the following: * ``"crypt"`` (see ), the password in the database should begin this $ * ``"ldap"`` (see https://tools.ietf.org/id/draft-stroeder-hashed-userpassword-values-01.html) the password in the database must begin with one of {MD5}, {SMD5}, {SHA}, {SSHA}, {SHA256}, {SSHA256}, {SHA384}, {SSHA384}, {SHA512}, {SSHA512}, {CRYPT}. * ``"hex_HASH_NAME"`` with ``HASH_NAME`` in md5, sha1, sha224, sha256, sha384, sha512. The hashed password in the database is compare to the hexadecimal digest of the clear password hashed with the corresponding algorithm. * ``"plain"``, the password in the database must be in clear. The default is ``"crypt"``. * ``CAS_SQL_PASSWORD_CHARSET``: Charset the SQL users passwords was hash with. This is needed to encode the user sended password before hashing it for comparison. The default is ``"utf-8"``. Ldap backend settings --------------------- Only usefull if you are using the ldap authentication backend: * ``CAS_LDAP_SERVER``: Address of the LDAP server. The default is ``"localhost"``. * ``CAS_LDAP_USER``: User bind address, for example ``"cn=admin,dc=crans,dc=org"`` for connecting to the LDAP server. * ``CAS_LDAP_PASSWORD``: Password for connecting to the LDAP server. * ``CAS_LDAP_BASE_DN``: LDAP search base DN, for example ``"ou=data,dc=crans,dc=org"``. * ``CAS_LDAP_USER_QUERY``: Search filter for searching user by username. User inputed usernames are escaped using ``ldap3.utils.conv.escape_bytes``. The default is ``"(uid=%s)"`` * ``CAS_LDAP_USERNAME_ATTR``: Attribute used for users usernames. The default is ``"uid"`` * ``CAS_LDAP_PASSWORD_ATTR``: Attribute used for users passwords. The default is ``"userPassword"`` * ``CAS_LDAP_PASSWORD_CHECK``: The method used to check the user password. Must be one of the following: * ``"crypt"`` (see ), the password in the database should begin this $ * ``"ldap"`` (see https://tools.ietf.org/id/draft-stroeder-hashed-userpassword-values-01.html) the password in the database must begin with one of {MD5}, {SMD5}, {SHA}, {SSHA}, {SHA256}, {SSHA256}, {SHA384}, {SSHA384}, {SHA512}, {SSHA512}, {CRYPT}. * ``"hex_HASH_NAME"`` with ``HASH_NAME`` in md5, sha1, sha224, sha256, sha384, sha512. The hashed password in the database is compare to the hexadecimal digest of the clear password hashed with the corresponding algorithm. * ``"plain"``, the password in the database must be in clear. * ``"bind``, the user credentials are used to bind to the ldap database and retreive the user attribute. In this mode, the settings ``CAS_LDAP_PASSWORD_ATTR`` and ``CAS_LDAP_PASSWORD_CHARSET`` are ignored, and it is the ldap server that perform password check. The counterpart is that the user attributes are only available upon user password check and so are cached for later use. All the other modes directly fetch the user attributes from the database whenever there are needed. This mean that is you use this mode, they can be some difference between the attributes in database and the cached ones if changes happend in the database after the user authentiate. See the parameter ``CAS_TGT_VALIDITY`` to force user to reauthenticate periodically. The default is ``"ldap"``. * ``CAS_LDAP_PASSWORD_CHARSET``: Charset the LDAP users passwords was hash with. This is needed to encode the user sended password before hashing it for comparison. The default is ``"utf-8"``. Test backend settings --------------------- Only usefull if you are using the test authentication backend: * ``CAS_TEST_USER``: Username of the test user. The default is ``"test"``. * ``CAS_TEST_PASSWORD``: Password of the test user. The default is ``"test"``. * ``CAS_TEST_ATTRIBUTES``: Attributes of the test user. The default is ``{'nom': 'Nymous', 'prenom': 'Ano', 'email': 'anonymous@example.net', 'alias': ['demo1', 'demo2']}``. Authentication backend ====================== ``django-cas-server`` comes with some authentication backends: * dummy backend ``cas_server.auth.DummyAuthUser``: all authentication attempt fails. * test backend ``cas_server.auth.TestAuthUser``: username, password and returned attributes for the user are defined by the ``CAS_TEST_*`` settings. * django backend ``cas_server.auth.DjangoAuthUser``: Users are authenticated against django users system. This is the default backend. The returned attributes are the fields available on the user model. * mysql backend ``cas_server.auth.MysqlAuthUser``: Deprecated, use the sql backend instead. see the `Mysql backend settings`_ section. The returned attributes are those return by sql query ``CAS_SQL_USER_QUERY``. * sql backend ``cas_server.auth.SqlAuthUser``: see the `Sql backend settings`_ section. The returned attributes are those return by sql query ``CAS_SQL_USER_QUERY``. * ldap backend ``cas_server.auth.LdapAuthUser``: see the `Ldap backend settings`_ section. The returned attributes are those of the ldap node returned by the query filter ``CAS_LDAP_USER_QUERY``. * federated backend ``cas_server.auth.CASFederateAuth``: It is automatically used then ``CAS_FEDERATE`` is ``True``. You should not set it manually without setting ``CAS_FEDERATE`` to ``True``. Logs ==== ``django-cas-server`` logs most of its actions. To enable login, you must set the ``LOGGING`` (https://docs.djangoproject.com/en/stable/topics/logging) variable in ``settings.py``. Users successful actions (login, logout) are logged with the level ``INFO``, failures are logged with the level ``WARNING`` and user attributes transmitted to a service are logged with the level ``DEBUG``. For example to log to syslog you can use : .. code-block:: python LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'cas_syslog': { 'format': 'cas: %(levelname)s %(message)s' }, }, 'handlers': { 'cas_syslog': { 'level': 'INFO', 'class': 'logging.handlers.SysLogHandler', 'address': '/dev/log', 'formatter': 'cas_syslog', }, }, 'loggers': { 'cas_server': { 'handlers': ['cas_syslog'], 'level': 'INFO', 'propagate': True, }, }, } Or to log to a file: .. code-block:: python LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'cas_file': { 'format': '%(asctime)s %(levelname)s %(message)s' }, }, 'handlers': { 'cas_file': { 'level': 'INFO', 'class': 'logging.FileHandler', 'filename': '/tmp/cas_server.log', 'formatter': 'cas_file', }, }, 'loggers': { 'cas_server': { 'handlers': ['cas_file'], 'level': 'INFO', 'propagate': True, }, }, } Service Patterns ================ In a CAS context, ``Service`` refers to the application the client is trying to access. By extension we use ``service`` for the URL of such an application. By default, ``django-cas-server`` do not allow any service to use the CAS to authenticate users. In order to allow services, you need to connect to the django admin interface using a django superuser, and add a first service pattern. A service pattern comes with 9 fields: * ``Position``: an integer used to change the order in which services are matched against service patterns. * ``Name``: the name of the service pattern. It will be displayed to the users asking for a ticket for a service matching this service pattern on the login page. * ``Pattern``: a regular expression used to match services. * ``User field``: the user attribute to use as username for services matching this service pattern. Leave it empty to use the login name. * ``Restrict username``: if checked, only login name defined below are allowed to get tickets for services matching this service pattern. * ``Proxy``: if checked, allow the creation of Proxy Ticket for services matching this service pattern. Otherwise, only Service Ticket will be created. * ``Proxy callback``: if checked, services matching this service pattern are allowed to retrieve Proxy Granting Ticket. A service with a Proxy Granting Ticket can get Proxy Ticket for other services. Hence you must only check this for trusted services that need it. (For instance, a webmail needs Proxy Ticket to authenticate himself as the user to the imap server). * ``Single log out``: Check it to send Single Log Out requests to authenticated services matching this service pattern. SLO requests are send to all services the user is authenticated to then the user disconnect. * ``Single log out callback``: The http(s) URL to POST the SLO requests. If empty, the service URL is used. This field is useful to allow non http services (imap, smtp, ftp) to handle SLO requests. A service pattern has 4 associated models: * ``Usernames``: a list of username associated with the ``Restrict username`` field * ``Replace attribut names``: a list of user attributes to send to the service. Choose the name used for sending the attribute by setting ``Remplacement`` or leave it empty to leave it unchanged. * ``Replace attribut values``: a list of sent user attributes for which value needs to be tweak. Replace the attribute value by the string obtained by replacing the leftmost non-overlapping occurrences of ``pattern`` in string by ``replace``. In ``replace`` backslash escapes are processed. Matched groups are captures by \1, \2, etc. * ``Filter attribut values``: a list of user attributes for which value needs to match a regular expression. For instance, service A may need an email address, and you only want user with an email address to connect to it. To do so, put ``email`` in ``Attribute`` and ``.*`` in ``pattern``. Then a user ask a ticket for a service, the service URL is compare against each service patterns sorted by ``position``. The first service pattern that matches the service URL is chosen. Hence, you should give low ``position`` to very specific patterns like ``^https://www\.example\.com(/.*)?$`` and higher ``position`` to generic patterns like ``^https://.*``. So the service URL ``https://www.examle.com`` will use the service pattern for ``^https://www\.example\.com(/.*)?$`` and not the one for ``^https://.*``. Federation mode =============== ``django-cas-server`` comes with a federation mode. Then ``CAS_FEDERATE`` is ``True``, user are invited to choose an identity provider on the login page, then, they are redirected to the provider CAS to authenticate. This provider transmit to ``django-cas-server`` the user username and attributes. The user is now logged in on ``django-cas-server`` and can use services using ``django-cas-server`` as CAS. In federation mode, the user attributes are cached upon user authentication. See the settings ``CAS_TGT_VALIDITY`` to force users to reauthenticate periodically and allow ``django-cas-server`` to refresh cached attributes. The list of allowed identity providers is defined using the django admin application. With the development server started, visit http://127.0.0.1:8000/admin/ to add identity providers. An identity provider comes with 5 fields: * ``Position``: an integer used to tweak the order in which identity providers are displayed on the login page. Identity providers are sorted using position first, then, on equal position, using ``verbose name`` and then, on equal ``verbose name``, using ``suffix``. * ``Suffix``: the suffix that will be append to the username returned by the identity provider. It must be unique. * ``Server url``: the URL to the identity provider CAS. For instance, if you are using ``https://cas.example.org/login`` to authenticate on the CAS, the ``server url`` is ``https://cas.example.org`` * ``CAS protocol version``: the version of the CAS protocol to use to contact the identity provider. The default is version 3. * ``Verbose name``: the name used on the login page to display the identity provider. * ``Display``: a boolean controlling the display of the identity provider on the login page. Beware that this do not disable the identity provider, it just hide it on the login page. User will always be able to log in using this provider by fetching ``/federate/provider_suffix``. In federation mode, ``django-cas-server`` build user's username as follow: ``provider_returned_username@provider_suffix``. Choose the provider returned username for ``django-cas-server`` and the provider suffix in order to make sense, as this built username is likely to be displayed to end users in applications. Then using federate mode, you should add one command to a daily crontab: ``cas_clean_federate``. This command clean the local cache of federated user from old unused users. You could for example do as bellow:: 10 0 * * * cas-user /path/to/project/manage.py cas_clean_federate .. |travis| image:: https://badges.genua.fr/travis/nitmir/django-cas-server/master.svg :target: https://travis-ci.org/nitmir/django-cas-server .. |pypi_version| image:: https://badges.genua.fr/pypi/v/django-cas-server.svg :target: https://pypi.org/project/django-cas-server/ .. |github_version| image:: https://badges.genua.fr/github/tag/nitmir/django-cas-server.svg?label=github :target: https://github.com/nitmir/django-cas-server/releases/latest .. |licence| image:: https://badges.genua.fr/pypi/l/django-cas-server.svg :target: https://www.gnu.org/licenses/gpl-3.0.html .. |codacy| image:: https://badges.genua.fr/codacy/grade/255c21623d6946ef8802fa7995b61366/master.svg :target: https://www.codacy.com/app/valentin-samir/django-cas-server .. |coverage| image:: https://intranet.genua.fr/coverage/badge/django-cas-server/master.svg :target: https://badges.genua.fr/coverage/django-cas-server/master .. |doc| image:: https://badges.genua.fr/local/readthedocs/?version=latest :target: http://django-cas-server.readthedocs.io django-cas-server-1.1.0/setup.py0000644000175000017500000000544713436457341020243 0ustar valentinvalentin00000000000000import os import pkg_resources from setuptools import setup from cas_server import VERSION with open(os.path.join(os.path.dirname(__file__), 'README.rst')) as readme: README = readme.read() if __name__ == '__main__': # allow setup.py to be run from any path os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) setup( name='django-cas-server', version=VERSION, packages=[ 'cas_server', 'cas_server.migrations', 'cas_server.management', 'cas_server.management.commands', 'cas_server.tests', 'cas_server.templatetags' ], include_package_data=True, license='GPLv3', description=( 'A Django Central Authentication Service server ' 'implementing the CAS Protocol 3.0 Specification' ), long_description=README, author='Valentin Samir', author_email='valentin.samir@crans.org', classifiers=[ 'Environment :: Web Environment', 'Development Status :: 5 - Production/Stable', 'Framework :: Django', 'Framework :: Django :: 1.11', 'Framework :: Django :: 2.0', 'Framework :: Django :: 2.1', 'Intended Audience :: Developers', 'Intended Audience :: System Administrators', 'License :: OSI Approved :: GNU General Public License v3 (GPLv3)', 'Operating System :: OS Independent', 'Programming Language :: Python', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Topic :: Software Development :: Libraries :: Python Modules', 'Topic :: Internet :: WWW/HTTP', 'Topic :: Internet :: WWW/HTTP :: Dynamic Content', 'Topic :: System :: Systems Administration :: Authentication/Directory' ], package_data={ 'cas_server': [ 'templates/cas_server/*', 'static/cas_server/*', 'locale/*/LC_MESSAGES/*', ] }, keywords=['django', 'cas', 'cas3', 'server', 'sso', 'single sign-on', 'authentication', 'auth'], install_requires=[ 'Django >= 1.11,<2.2', 'requests >= 2.4', 'requests_futures >= 0.9.5', 'lxml >= 3.4', 'six >= 1' ], url="https://github.com/nitmir/django-cas-server", download_url="https://github.com/nitmir/django-cas-server/releases/latest", zip_safe=False, setup_requires=['pytest-runner'], tests_require=['pytest', 'pytest-django', 'pytest-pythonpath', 'pytest-warnings', 'mock>=1'], ) django-cas-server-1.1.0/django_cas_server.egg-info/0000755000175000017500000000000013436457571023674 5ustar valentinvalentin00000000000000django-cas-server-1.1.0/django_cas_server.egg-info/dependency_links.txt0000644000175000017500000000000113436457571027742 0ustar valentinvalentin00000000000000 django-cas-server-1.1.0/django_cas_server.egg-info/SOURCES.txt0000644000175000017500000000565213436457571025570 0ustar valentinvalentin00000000000000.coveragerc CHANGELOG.rst LICENSE MANIFEST.in Makefile README.rst pytest.ini requirements-dev.txt requirements.txt setup.cfg setup.py tox.ini cas_server/__init__.py cas_server/admin.py cas_server/apps.py cas_server/auth.py cas_server/cas.py cas_server/default_settings.py cas_server/federate.py cas_server/forms.py cas_server/models.py cas_server/urls.py cas_server/utils.py cas_server/views.py cas_server/locale/fr/LC_MESSAGES/django.mo cas_server/locale/fr/LC_MESSAGES/django.po cas_server/locale/nl/LC_MESSAGES/django.mo cas_server/locale/nl/LC_MESSAGES/django.po cas_server/locale/pt_BR/django.mo cas_server/locale/pt_BR/django.po cas_server/management/__init__.py cas_server/management/commands/__init__.py cas_server/management/commands/cas_clean_federate.py cas_server/management/commands/cas_clean_sessions.py cas_server/management/commands/cas_clean_tickets.py cas_server/migrations/0001_squashed_0013_auto_20170329_1748.py cas_server/migrations/__init__.py cas_server/static/cas_server/cas.js cas_server/static/cas_server/favicon.ico cas_server/static/cas_server/functions.js cas_server/static/cas_server/logo.png cas_server/static/cas_server/logo.svg cas_server/static/cas_server/styles.css cas_server/templates/cas_server/base.html cas_server/templates/cas_server/form.html cas_server/templates/cas_server/logged.html cas_server/templates/cas_server/login.html cas_server/templates/cas_server/logout.html cas_server/templates/cas_server/proxy.xml cas_server/templates/cas_server/samlValidate.xml cas_server/templates/cas_server/samlValidateError.xml cas_server/templates/cas_server/serviceValidate.xml cas_server/templates/cas_server/serviceValidateError.xml cas_server/templates/cas_server/warn.html cas_server/templatetags/__init__.py cas_server/templatetags/cas_server.py cas_server/tests/__init__.py cas_server/tests/auth.py cas_server/tests/mixin.py cas_server/tests/settings.py cas_server/tests/test_federate.py cas_server/tests/test_models.py cas_server/tests/test_templatetags.py cas_server/tests/test_utils.py cas_server/tests/test_view.py cas_server/tests/urls.py cas_server/tests/utils.py django_cas_server.egg-info/PKG-INFO django_cas_server.egg-info/SOURCES.txt django_cas_server.egg-info/dependency_links.txt django_cas_server.egg-info/not-zip-safe django_cas_server.egg-info/requires.txt django_cas_server.egg-info/top_level.txt docs/CHANGELOG.rst docs/Makefile docs/README.rst docs/conf.py docs/index.rst docs/_ext/djangodocs.py docs/package/cas_server.admin.rst docs/package/cas_server.apps.rst docs/package/cas_server.auth.rst docs/package/cas_server.cas.rst docs/package/cas_server.default_settings.rst docs/package/cas_server.federate.rst docs/package/cas_server.forms.rst docs/package/cas_server.models.rst docs/package/cas_server.rst docs/package/cas_server.templatetags.cas_server.rst docs/package/cas_server.templatetags.rst docs/package/cas_server.urls.rst docs/package/cas_server.utils.rst docs/package/cas_server.views.rst docs/package/modules.rstdjango-cas-server-1.1.0/django_cas_server.egg-info/PKG-INFO0000644000175000017500000011400413436457571024771 0ustar valentinvalentin00000000000000Metadata-Version: 1.1 Name: django-cas-server Version: 1.1.0 Summary: A Django Central Authentication Service server implementing the CAS Protocol 3.0 Specification Home-page: https://github.com/nitmir/django-cas-server Author: Valentin Samir Author-email: valentin.samir@crans.org License: GPLv3 Download-URL: https://github.com/nitmir/django-cas-server/releases/latest Description: CAS Server ########## |travis| |coverage| |licence| |github_version| |pypi_version| |codacy| |doc| CAS Server is a Django application implementing the `CAS Protocol 3.0 Specification `_. By default, the authentication process use django internal users but you can easily use any sources (see the `Authentication backend`_ section and auth classes in the auth.py file) .. contents:: Table of Contents Features ======== * Support CAS version 1.0, 2.0, 3.0 * Support Single Sign Out * Configuration of services via the django Admin application * Fine control on which user's attributes are passed to which service * Possibility to rename/rewrite attributes per service * Possibility to require some attribute values per service * Federated mode between multiple CAS * Supports Django 1.11 and 2.0 * Supports Python 2.7, 3.5+ Dependencies ============ ``django-cas-server`` depends on the following python packages: * Django >= 1.11 < 2.1 * requests >= 2.4 * requests_futures >= 0.9.5 * lxml >= 3.4 * six >= 1.8 Minimal version of packages dependancy are just indicative and meens that ``django-cas-server`` has been tested with it. Previous versions of dependencies may or may not work. Additionally, denpending of the `Authentication backend`_ you plan to use, you may need the following python packages: * ldap3 * psycopg2 * mysql-python Here there is a table with the name of python packages and the corresponding packages providing them on debian like systems and centos like systems. You should try as much as possible to use system packages as there are automatically updated then you update your system. You can then install Not Available (N/A) packages on your system using pip inside a virtualenv as described in the `Installation`_ section. For use with python3, just replace python(2) in the table by python3. +------------------+-------------------------+---------------------+ | python package | debian like systems | centos like systems | +==================+=========================+=====================+ | Django | python-django | python-django | +------------------+-------------------------+---------------------+ | requests | python-requests | python-requests | +------------------+-------------------------+---------------------+ | requests_futures | python-requests-futures | N/A | +------------------+-------------------------+---------------------+ | lxml | python-lxml | python-lxml | +------------------+-------------------------+---------------------+ | six | python-six | python-six | +------------------+-------------------------+---------------------+ | ldap3 | python-ldap3 | python-ldap3 | +------------------+-------------------------+---------------------+ | psycopg2 | python-psycopg2 | python-psycopg2 | +------------------+-------------------------+---------------------+ | mysql-python | python-mysqldb | python2-mysql | +------------------+-------------------------+---------------------+ Installation ============ The recommended installation mode is to use a virtualenv with ``--system-site-packages`` 1. Make sure that python virtualenv is installed 2. Install python packages available via the system package manager: On debian like systems:: $ sudo apt-get install python-django python-requests python-six python-lxml python-requests-futures On debian jessie, you can use the version of python-django available in the `backports `_. On centos like systems:: $ sudo yum install python-django python-requests python-six python-lxml 3. Create a virtualenv:: $ virtualenv --system-site-packages cas_venv Running virtualenv with interpreter /var/www/html/cas-server/bin/python2 Using real prefix '/usr' New python executable in cas/bin/python2 Also creating executable in cas/bin/python Installing setuptools, pip...done. 4. And `activate it `__:: $ cd cas_venv/; . bin/activate 5. Create a django project:: $ django-admin startproject cas_project $ cd cas_project 6. Install `django-cas-server`. To use the last published release, run:: $ pip install django-cas-server Alternatively if you want to use the version of the git repository, you can clone it:: $ git clone https://github.com/nitmir/django-cas-server $ cd django-cas-server $ pip install -r requirements.txt Then, either run ``make install`` to create a python package using the sources of the repository and install it with pip, or place the ``cas_server`` directory into your `PYTHONPATH `_ (for instance by symlinking ``cas_server`` to the root of your django project). 7. Open ``cas_project/settings.py`` in you favourite editor and follow the quick start section. Quick start =========== 1. Add "cas_server" to your INSTALLED_APPS setting like this:: INSTALLED_APPS = ( 'django.contrib.admin', ... 'cas_server', ) For internationalization support, add "django.middleware.locale.LocaleMiddleware" to your MIDDLEWARE_CLASSES setting like this:: MIDDLEWARE_CLASSES = ( ... 'django.middleware.locale.LocaleMiddleware', ... ) 2. Include the cas_server URLconf in your project urls.py like this:: from django.conf.urls import url, include urlpatterns = [ url(r'^admin/', admin.site.urls), ... url(r'^cas/', include('cas_server.urls', namespace="cas_server")), ] 3. Run ``python manage.py migrate`` to create the cas_server models. 4. You should add some management commands to a crontab: ``clearsessions``, ``cas_clean_tickets`` and ``cas_clean_sessions``. * ``clearsessions``: please see `Clearing the session store `_. * ``cas_clean_tickets``: old tickets and timed-out tickets do not get purge from the database automatically. They are just marked as invalid. ``cas_clean_tickets`` is a clean-up management command for this purpose. It send SingleLogOut request to services with timed out tickets and delete them. * ``cas_clean_sessions``: Logout and purge users (sending SLO requests) that are inactive since more than ``SESSION_COOKIE_AGE``. The default value for is ``1209600`` seconds (2 weeks). You probably should reduce it to something like ``86400`` seconds (1 day). You could for example do as bellow:: 0 0 * * * cas-user /path/to/project/manage.py clearsessions */5 * * * * cas-user /path/to/project/manage.py cas_clean_tickets 5 0 * * * cas-user /path/to/project/manage.py cas_clean_sessions 5. Run ``python manage.py createsuperuser`` to create an administrator user. 6. Start the development server and visit http://127.0.0.1:8000/admin/ to add a first service allowed to authenticate user against the CAS (you'll need the Admin app enabled). See the `Service Patterns`_ section bellow. 7. Visit http://127.0.0.1:8000/cas/ to login with your django users. Settings ======== All settings are optional. Add them to ``settings.py`` to customize ``django-cas-server``: Template settings ----------------- * ``CAS_LOGO_URL``: URL to the logo showed in the up left corner on the default templates. Set it to ``False`` to disable it. * ``CAS_FAVICON_URL``: URL to the favicon (shortcut icon) used by the default templates. Default is a key icon. Set it to ``False`` to disable it. * ``CAS_SHOW_POWERED``: Set it to ``False`` to hide the powered by footer. The default is ``True``. * ``CAS_COMPONENT_URLS``: URLs to css and javascript external components. It is a dictionnary having the five following keys: ``"bootstrap3_css"``, ``"bootstrap3_js"``, ``"html5shiv"``, ``"respond"``, ``"jquery"``. The default is:: { "bootstrap3_css": "//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css", "bootstrap3_js": "//maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js", "html5shiv": "//oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js", "respond": "//oss.maxcdn.com/libs/respond.js/1.4.2/respond.min.js", "jquery": "//code.jquery.com/jquery.min.js", } if you omit some keys of the dictionnary, the default value for these keys is used. * ``CAS_SHOW_SERVICE_MESSAGES``: Messages displayed about the state of the service on the login page. The default is ``True``. * ``CAS_INFO_MESSAGES``: Messages displayed in info-boxes on the html pages of the default templates. It is a dictionnary mapping message name to a message dict. A message dict has 3 keys: * ``message``: A unicode message to display, potentially wrapped around ugettex_lazy * ``discardable``: A boolean, specify if the users can close the message info-box * ``type``: One of info, success, info, warning, danger. The type of the info-box. ``CAS_INFO_MESSAGES`` contains by default one message, ``cas_explained``, which explain roughly the purpose of a CAS. The default is:: { "cas_explained": { "message":_( u"The Central Authentication Service grants you access to most of our websites by " u"authenticating only once, so you don't need to type your credentials again unless " u"your session expires or you logout." ), "discardable": True, "type": "info", # one of info, success, info, warning, danger }, } * ``CAS_INFO_MESSAGES_ORDER``: A list of message names. Order in which info-box messages are displayed. Use an empty list to disable messages display. The default is ``[]``. * ``CAS_LOGIN_TEMPLATE``: Path to the template showed on ``/login`` then the user is not autenticated. The default is ``"cas_server/login.html"``. * ``CAS_WARN_TEMPLATE``: Path to the template showed on ``/login?service=...`` then the user is authenticated and has asked to be warned before being connected to a service. The default is ``"cas_server/warn.html"``. * ``CAS_LOGGED_TEMPLATE``: Path to the template showed on ``/login`` then to user is authenticated. The default is ``"cas_server/logged.html"``. * ``CAS_LOGOUT_TEMPLATE``: Path to the template showed on ``/logout`` then to user is being disconnected. The default is ``"cas_server/logout.html"`` * ``CAS_REDIRECT_TO_LOGIN_AFTER_LOGOUT``: Should we redirect users to ``/login`` after they logged out instead of displaying ``CAS_LOGOUT_TEMPLATE``. The default is ``False``. Authentication settings ----------------------- * ``CAS_AUTH_CLASS``: A dotted path to a class or a class implementing ``cas_server.auth.AuthUser``. The default is ``"cas_server.auth.DjangoAuthUser"`` Available classes bundled with ``django-cas-server`` are listed below in the `Authentication backend`_ section. * ``SESSION_COOKIE_AGE``: This is a django settings. Here, it control the delay in seconds after which inactive users are logged out. The default is ``1209600`` (2 weeks). You probably should reduce it to something like ``86400`` seconds (1 day). * ``CAS_TGT_VALIDITY``: Max time after with the user MUST reauthenticate. Let it to `None` for no max time.This can be used to force refreshing cached informations only available upon user authentication like the user attributes in federation mode or with the ldap auth in bind mode. The default is ``None``. * ``CAS_PROXY_CA_CERTIFICATE_PATH``: Path to certificate authorities file. Usually on linux the local CAs are in ``/etc/ssl/certs/ca-certificates.crt``. The default is ``True`` which tell requests to use its internal certificat authorities. Settings it to ``False`` should disable all x509 certificates validation and MUST not be done in production. x509 certificate validation is perform upon PGT issuance. * ``CAS_SLO_MAX_PARALLEL_REQUESTS``: Maximum number of parallel single log out requests send. If more requests need to be send, there are queued. The default is ``10``. * ``CAS_SLO_TIMEOUT``: Timeout for a single SLO request in seconds. The default is ``5``. Federation settings ------------------- * ``CAS_FEDERATE``: A boolean for activating the federated mode (see the `Federation mode`_ section below). The default is ``False``. * ``CAS_FEDERATE_REMEMBER_TIMEOUT``: Time after witch the cookie use for "remember my identity provider" expire. The default is ``604800``, one week. The cookie is called ``_remember_provider``. New version warnings settings ----------------------------- * ``CAS_NEW_VERSION_HTML_WARNING``: A boolean for diplaying a warning on html pages then a new version of the application is avaible. Once closed by a user, it is not displayed to this user until the next new version. The default is ``True``. * ``CAS_NEW_VERSION_EMAIL_WARNING``: A boolean for sending a email to ``settings.ADMINS`` when a new version is available. The default is ``True``. Tickets validity settings ------------------------- * ``CAS_TICKET_VALIDITY``: Number of seconds the service tickets and proxy tickets are valid. This is the maximal time between ticket issuance by the CAS and ticket validation by an application. The default is ``60``. * ``CAS_PGT_VALIDITY``: Number of seconds the proxy granting tickets are valid. The default is ``3600`` (1 hour). * ``CAS_TICKET_TIMEOUT``: Number of seconds a ticket is kept in the database before sending Single Log Out request and being cleared. The default is ``86400`` (24 hours). Tickets miscellaneous settings ------------------------------ * ``CAS_TICKET_LEN``: Default ticket length. All CAS implementation MUST support ST and PT up to 32 chars, PGT and PGTIOU up to 64 chars and it is RECOMMENDED that all tickets up to 256 chars are supports. Here the default is ``64``. * ``CAS_LT_LEN``: Length of the login tickets. Login tickets are only processed by ``django-cas-server`` thus there is no length restriction on it. The default is ``CAS_TICKET_LEN``. * ``CAS_ST_LEN``: Length of the service tickets. The default is ``CAS_TICKET_LEN``. You may need to lower is to ``32`` if you use some old clients. * ``CAS_PT_LEN``: Length of the proxy tickets. The default is ``CAS_TICKET_LEN``. This length should be the same as ``CAS_ST_LEN``. You may need to lower is to ``32`` if you use some old clients. * ``CAS_PGT_LEN``: Length of the proxy granting tickets. The default is ``CAS_TICKET_LEN``. * ``CAS_PGTIOU_LEN``: Length of the proxy granting tickets IOU. The default is ``CAS_TICKET_LEN``. * ``CAS_LOGIN_TICKET_PREFIX``: Prefix of login tickets. The default is ``"LT"``. * ``CAS_SERVICE_TICKET_PREFIX``: Prefix of service tickets. The default is ``"ST"``. The CAS specification mandate that service tickets MUST begin with the characters ST so you should not change this. * ``CAS_PROXY_TICKET_PREFIX``: Prefix of proxy ticket. The default is ``"PT"``. * ``CAS_PROXY_GRANTING_TICKET_PREFIX``: Prefix of proxy granting ticket. The default is ``"PGT"``. * ``CAS_PROXY_GRANTING_TICKET_IOU_PREFIX``: Prefix of proxy granting ticket IOU. The default is ``"PGTIOU"``. Mysql backend settings ---------------------- Deprecated, see the `Sql backend settings`_. Only usefull if you are using the mysql authentication backend: * ``CAS_SQL_HOST``: Host for the SQL server. The default is ``"localhost"``. * ``CAS_SQL_USERNAME``: Username for connecting to the SQL server. * ``CAS_SQL_PASSWORD``: Password for connecting to the SQL server. * ``CAS_SQL_DBNAME``: Database name. * ``CAS_SQL_DBCHARSET``: Database charset. The default is ``"utf8"`` * ``CAS_SQL_USER_QUERY``: The query performed upon user authentication. The username must be in field ``username``, the password in ``password``, additional fields are used as the user attributes. The default is ``"SELECT user AS username, pass AS password, users.* FROM users WHERE user = %s"`` * ``CAS_SQL_PASSWORD_CHECK``: The method used to check the user password. Must be one of the following: * ``"crypt"`` (see ), the password in the database should begin this $ * ``"ldap"`` (see https://tools.ietf.org/id/draft-stroeder-hashed-userpassword-values-01.html) the password in the database must begin with one of {MD5}, {SMD5}, {SHA}, {SSHA}, {SHA256}, {SSHA256}, {SHA384}, {SSHA384}, {SHA512}, {SSHA512}, {CRYPT}. * ``"hex_HASH_NAME"`` with ``HASH_NAME`` in md5, sha1, sha224, sha256, sha384, sha512. The hashed password in the database is compare to the hexadecimal digest of the clear password hashed with the corresponding algorithm. * ``"plain"``, the password in the database must be in clear. The default is ``"crypt"``. Sql backend settings -------------------- Only usefull if you are using the sql authentication backend. You must add a ``"cas_server"`` database to `settings.DATABASES `__ as defined in the django documentation. It is then the database use by the sql backend. * ``CAS_SQL_USER_QUERY``: The query performed upon user authentication. The username must be in field ``username``, the password in ``password``, additional fields are used as the user attributes. The default is ``"SELECT user AS username, pass AS password, users.* FROM users WHERE user = %s"`` * ``CAS_SQL_PASSWORD_CHECK``: The method used to check the user password. Must be one of the following: * ``"crypt"`` (see ), the password in the database should begin this $ * ``"ldap"`` (see https://tools.ietf.org/id/draft-stroeder-hashed-userpassword-values-01.html) the password in the database must begin with one of {MD5}, {SMD5}, {SHA}, {SSHA}, {SHA256}, {SSHA256}, {SHA384}, {SSHA384}, {SHA512}, {SSHA512}, {CRYPT}. * ``"hex_HASH_NAME"`` with ``HASH_NAME`` in md5, sha1, sha224, sha256, sha384, sha512. The hashed password in the database is compare to the hexadecimal digest of the clear password hashed with the corresponding algorithm. * ``"plain"``, the password in the database must be in clear. The default is ``"crypt"``. * ``CAS_SQL_PASSWORD_CHARSET``: Charset the SQL users passwords was hash with. This is needed to encode the user sended password before hashing it for comparison. The default is ``"utf-8"``. Ldap backend settings --------------------- Only usefull if you are using the ldap authentication backend: * ``CAS_LDAP_SERVER``: Address of the LDAP server. The default is ``"localhost"``. * ``CAS_LDAP_USER``: User bind address, for example ``"cn=admin,dc=crans,dc=org"`` for connecting to the LDAP server. * ``CAS_LDAP_PASSWORD``: Password for connecting to the LDAP server. * ``CAS_LDAP_BASE_DN``: LDAP search base DN, for example ``"ou=data,dc=crans,dc=org"``. * ``CAS_LDAP_USER_QUERY``: Search filter for searching user by username. User inputed usernames are escaped using ``ldap3.utils.conv.escape_bytes``. The default is ``"(uid=%s)"`` * ``CAS_LDAP_USERNAME_ATTR``: Attribute used for users usernames. The default is ``"uid"`` * ``CAS_LDAP_PASSWORD_ATTR``: Attribute used for users passwords. The default is ``"userPassword"`` * ``CAS_LDAP_PASSWORD_CHECK``: The method used to check the user password. Must be one of the following: * ``"crypt"`` (see ), the password in the database should begin this $ * ``"ldap"`` (see https://tools.ietf.org/id/draft-stroeder-hashed-userpassword-values-01.html) the password in the database must begin with one of {MD5}, {SMD5}, {SHA}, {SSHA}, {SHA256}, {SSHA256}, {SHA384}, {SSHA384}, {SHA512}, {SSHA512}, {CRYPT}. * ``"hex_HASH_NAME"`` with ``HASH_NAME`` in md5, sha1, sha224, sha256, sha384, sha512. The hashed password in the database is compare to the hexadecimal digest of the clear password hashed with the corresponding algorithm. * ``"plain"``, the password in the database must be in clear. * ``"bind``, the user credentials are used to bind to the ldap database and retreive the user attribute. In this mode, the settings ``CAS_LDAP_PASSWORD_ATTR`` and ``CAS_LDAP_PASSWORD_CHARSET`` are ignored, and it is the ldap server that perform password check. The counterpart is that the user attributes are only available upon user password check and so are cached for later use. All the other modes directly fetch the user attributes from the database whenever there are needed. This mean that is you use this mode, they can be some difference between the attributes in database and the cached ones if changes happend in the database after the user authentiate. See the parameter ``CAS_TGT_VALIDITY`` to force user to reauthenticate periodically. The default is ``"ldap"``. * ``CAS_LDAP_PASSWORD_CHARSET``: Charset the LDAP users passwords was hash with. This is needed to encode the user sended password before hashing it for comparison. The default is ``"utf-8"``. Test backend settings --------------------- Only usefull if you are using the test authentication backend: * ``CAS_TEST_USER``: Username of the test user. The default is ``"test"``. * ``CAS_TEST_PASSWORD``: Password of the test user. The default is ``"test"``. * ``CAS_TEST_ATTRIBUTES``: Attributes of the test user. The default is ``{'nom': 'Nymous', 'prenom': 'Ano', 'email': 'anonymous@example.net', 'alias': ['demo1', 'demo2']}``. Authentication backend ====================== ``django-cas-server`` comes with some authentication backends: * dummy backend ``cas_server.auth.DummyAuthUser``: all authentication attempt fails. * test backend ``cas_server.auth.TestAuthUser``: username, password and returned attributes for the user are defined by the ``CAS_TEST_*`` settings. * django backend ``cas_server.auth.DjangoAuthUser``: Users are authenticated against django users system. This is the default backend. The returned attributes are the fields available on the user model. * mysql backend ``cas_server.auth.MysqlAuthUser``: Deprecated, use the sql backend instead. see the `Mysql backend settings`_ section. The returned attributes are those return by sql query ``CAS_SQL_USER_QUERY``. * sql backend ``cas_server.auth.SqlAuthUser``: see the `Sql backend settings`_ section. The returned attributes are those return by sql query ``CAS_SQL_USER_QUERY``. * ldap backend ``cas_server.auth.LdapAuthUser``: see the `Ldap backend settings`_ section. The returned attributes are those of the ldap node returned by the query filter ``CAS_LDAP_USER_QUERY``. * federated backend ``cas_server.auth.CASFederateAuth``: It is automatically used then ``CAS_FEDERATE`` is ``True``. You should not set it manually without setting ``CAS_FEDERATE`` to ``True``. Logs ==== ``django-cas-server`` logs most of its actions. To enable login, you must set the ``LOGGING`` (https://docs.djangoproject.com/en/stable/topics/logging) variable in ``settings.py``. Users successful actions (login, logout) are logged with the level ``INFO``, failures are logged with the level ``WARNING`` and user attributes transmitted to a service are logged with the level ``DEBUG``. For example to log to syslog you can use : .. code-block:: python LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'cas_syslog': { 'format': 'cas: %(levelname)s %(message)s' }, }, 'handlers': { 'cas_syslog': { 'level': 'INFO', 'class': 'logging.handlers.SysLogHandler', 'address': '/dev/log', 'formatter': 'cas_syslog', }, }, 'loggers': { 'cas_server': { 'handlers': ['cas_syslog'], 'level': 'INFO', 'propagate': True, }, }, } Or to log to a file: .. code-block:: python LOGGING = { 'version': 1, 'disable_existing_loggers': False, 'formatters': { 'cas_file': { 'format': '%(asctime)s %(levelname)s %(message)s' }, }, 'handlers': { 'cas_file': { 'level': 'INFO', 'class': 'logging.FileHandler', 'filename': '/tmp/cas_server.log', 'formatter': 'cas_file', }, }, 'loggers': { 'cas_server': { 'handlers': ['cas_file'], 'level': 'INFO', 'propagate': True, }, }, } Service Patterns ================ In a CAS context, ``Service`` refers to the application the client is trying to access. By extension we use ``service`` for the URL of such an application. By default, ``django-cas-server`` do not allow any service to use the CAS to authenticate users. In order to allow services, you need to connect to the django admin interface using a django superuser, and add a first service pattern. A service pattern comes with 9 fields: * ``Position``: an integer used to change the order in which services are matched against service patterns. * ``Name``: the name of the service pattern. It will be displayed to the users asking for a ticket for a service matching this service pattern on the login page. * ``Pattern``: a regular expression used to match services. * ``User field``: the user attribute to use as username for services matching this service pattern. Leave it empty to use the login name. * ``Restrict username``: if checked, only login name defined below are allowed to get tickets for services matching this service pattern. * ``Proxy``: if checked, allow the creation of Proxy Ticket for services matching this service pattern. Otherwise, only Service Ticket will be created. * ``Proxy callback``: if checked, services matching this service pattern are allowed to retrieve Proxy Granting Ticket. A service with a Proxy Granting Ticket can get Proxy Ticket for other services. Hence you must only check this for trusted services that need it. (For instance, a webmail needs Proxy Ticket to authenticate himself as the user to the imap server). * ``Single log out``: Check it to send Single Log Out requests to authenticated services matching this service pattern. SLO requests are send to all services the user is authenticated to then the user disconnect. * ``Single log out callback``: The http(s) URL to POST the SLO requests. If empty, the service URL is used. This field is useful to allow non http services (imap, smtp, ftp) to handle SLO requests. A service pattern has 4 associated models: * ``Usernames``: a list of username associated with the ``Restrict username`` field * ``Replace attribut names``: a list of user attributes to send to the service. Choose the name used for sending the attribute by setting ``Remplacement`` or leave it empty to leave it unchanged. * ``Replace attribut values``: a list of sent user attributes for which value needs to be tweak. Replace the attribute value by the string obtained by replacing the leftmost non-overlapping occurrences of ``pattern`` in string by ``replace``. In ``replace`` backslash escapes are processed. Matched groups are captures by \1, \2, etc. * ``Filter attribut values``: a list of user attributes for which value needs to match a regular expression. For instance, service A may need an email address, and you only want user with an email address to connect to it. To do so, put ``email`` in ``Attribute`` and ``.*`` in ``pattern``. Then a user ask a ticket for a service, the service URL is compare against each service patterns sorted by ``position``. The first service pattern that matches the service URL is chosen. Hence, you should give low ``position`` to very specific patterns like ``^https://www\.example\.com(/.*)?$`` and higher ``position`` to generic patterns like ``^https://.*``. So the service URL ``https://www.examle.com`` will use the service pattern for ``^https://www\.example\.com(/.*)?$`` and not the one for ``^https://.*``. Federation mode =============== ``django-cas-server`` comes with a federation mode. Then ``CAS_FEDERATE`` is ``True``, user are invited to choose an identity provider on the login page, then, they are redirected to the provider CAS to authenticate. This provider transmit to ``django-cas-server`` the user username and attributes. The user is now logged in on ``django-cas-server`` and can use services using ``django-cas-server`` as CAS. In federation mode, the user attributes are cached upon user authentication. See the settings ``CAS_TGT_VALIDITY`` to force users to reauthenticate periodically and allow ``django-cas-server`` to refresh cached attributes. The list of allowed identity providers is defined using the django admin application. With the development server started, visit http://127.0.0.1:8000/admin/ to add identity providers. An identity provider comes with 5 fields: * ``Position``: an integer used to tweak the order in which identity providers are displayed on the login page. Identity providers are sorted using position first, then, on equal position, using ``verbose name`` and then, on equal ``verbose name``, using ``suffix``. * ``Suffix``: the suffix that will be append to the username returned by the identity provider. It must be unique. * ``Server url``: the URL to the identity provider CAS. For instance, if you are using ``https://cas.example.org/login`` to authenticate on the CAS, the ``server url`` is ``https://cas.example.org`` * ``CAS protocol version``: the version of the CAS protocol to use to contact the identity provider. The default is version 3. * ``Verbose name``: the name used on the login page to display the identity provider. * ``Display``: a boolean controlling the display of the identity provider on the login page. Beware that this do not disable the identity provider, it just hide it on the login page. User will always be able to log in using this provider by fetching ``/federate/provider_suffix``. In federation mode, ``django-cas-server`` build user's username as follow: ``provider_returned_username@provider_suffix``. Choose the provider returned username for ``django-cas-server`` and the provider suffix in order to make sense, as this built username is likely to be displayed to end users in applications. Then using federate mode, you should add one command to a daily crontab: ``cas_clean_federate``. This command clean the local cache of federated user from old unused users. You could for example do as bellow:: 10 0 * * * cas-user /path/to/project/manage.py cas_clean_federate .. |travis| image:: https://badges.genua.fr/travis/nitmir/django-cas-server/master.svg :target: https://travis-ci.org/nitmir/django-cas-server .. |pypi_version| image:: https://badges.genua.fr/pypi/v/django-cas-server.svg :target: https://pypi.org/project/django-cas-server/ .. |github_version| image:: https://badges.genua.fr/github/tag/nitmir/django-cas-server.svg?label=github :target: https://github.com/nitmir/django-cas-server/releases/latest .. |licence| image:: https://badges.genua.fr/pypi/l/django-cas-server.svg :target: https://www.gnu.org/licenses/gpl-3.0.html .. |codacy| image:: https://badges.genua.fr/codacy/grade/255c21623d6946ef8802fa7995b61366/master.svg :target: https://www.codacy.com/app/valentin-samir/django-cas-server .. |coverage| image:: https://intranet.genua.fr/coverage/badge/django-cas-server/master.svg :target: https://badges.genua.fr/coverage/django-cas-server/master .. |doc| image:: https://badges.genua.fr/local/readthedocs/?version=latest :target: http://django-cas-server.readthedocs.io Keywords: django,cas,cas3,server,sso,single sign-on,authentication,auth Platform: UNKNOWN Classifier: Environment :: Web Environment Classifier: Development Status :: 5 - Production/Stable Classifier: Framework :: Django Classifier: Framework :: Django :: 1.11 Classifier: Framework :: Django :: 2.0 Classifier: Framework :: Django :: 2.1 Classifier: Intended Audience :: Developers Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3) Classifier: Operating System :: OS Independent Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Topic :: Software Development :: Libraries :: Python Modules Classifier: Topic :: Internet :: WWW/HTTP Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content Classifier: Topic :: System :: Systems Administration :: Authentication/Directory django-cas-server-1.1.0/django_cas_server.egg-info/not-zip-safe0000644000175000017500000000000113436457571026122 0ustar valentinvalentin00000000000000 django-cas-server-1.1.0/django_cas_server.egg-info/top_level.txt0000644000175000017500000000001313436457571026420 0ustar valentinvalentin00000000000000cas_server django-cas-server-1.1.0/django_cas_server.egg-info/requires.txt0000644000175000017500000000012313436457571026270 0ustar valentinvalentin00000000000000Django >= 1.11,<2.2 lxml >= 3.4 requests >= 2.4 requests_futures >= 0.9.5 six >= 1 django-cas-server-1.1.0/MANIFEST.in0000644000175000017500000000106112757406463020257 0ustar valentinvalentin00000000000000include tox.ini include LICENSE include README.rst include CHANGELOG.rst include .coveragerc include Makefile include pytest.ini include requirements-dev.txt include requirements.txt prune .tox recursive-include cas_server/static * recursive-include cas_server/templates * recursive-include cas_server/locale * include docs/conf.py include docs/index.rst include docs/Makefile include docs/README.rst include docs/CHANGELOG.rst recursive-include docs/_ext * recursive-include docs/package * recursive-include docs/_static * recursive-include docs/_templates * django-cas-server-1.1.0/.coveragerc0000644000175000017500000000047612757022243020641 0ustar valentinvalentin00000000000000[run] branch = True source = cas_server omit = cas_server/migrations* cas_server/management/* cas_server/tests/* cas_server/cas.py [report] exclude_lines = pragma: no cover def __repr__ def __unicode__ def __str__ raise AssertionError raise NotImplementedError if six.PY3: django-cas-server-1.1.0/pytest.ini0000644000175000017500000000021612757022243020541 0ustar valentinvalentin00000000000000[pytest] testpaths = cas_server/tests/ DJANGO_SETTINGS_MODULE = cas_server.tests.settings norecursedirs = .* build dist docs python_paths = . django-cas-server-1.1.0/Makefile0000644000175000017500000000516013416345433020155 0ustar valentinvalentin00000000000000.PHONY: build dist docs VERSION=`python setup.py -V` build: python setup.py build install: dist pip -V pip install --no-cache-dir --no-deps --upgrade --force-reinstall --find-links ./dist/django-cas-server-${VERSION}.tar.gz django-cas-server uninstall: pip uninstall django-cas-server || true clean_pyc: find ./ -name '*.pyc' -delete find ./ -name __pycache__ -delete clean_build: rm -rf build django_cas_server.egg-info dist clean_tox: rm -rf .tox tox_logs clean_test_venv: rm -rf test_venv clean_coverage: rm -rf coverage.xml .coverage htmlcov clean_tild_backup: find ./ -name '*~' -delete clean_docs: rm -rf docs/_build/ docs/django.inv clean_eggs: rm -rf .eggs/ clean: clean_pyc clean_build clean_coverage clean_tild_backup clean_all: clean clean_tox clean_test_venv clean_docs clean_eggs dist: python setup.py sdist test_venv/bin/python: python3 -m venv test_venv test_venv/bin/pip install -U --requirement requirements-dev.txt 'Django>=2.0,<2.1' test_venv/cas/manage.py: test_venv mkdir -p test_venv/cas test_venv/bin/django-admin startproject cas test_venv/cas ln -s ../../cas_server test_venv/cas/cas_server sed -i "s/'django.contrib.staticfiles',/'django.contrib.staticfiles',\n 'cas_server',/" test_venv/cas/cas/settings.py sed -i "s/'django.middleware.clickjacking.XFrameOptionsMiddleware',/'django.middleware.clickjacking.XFrameOptionsMiddleware',\n 'django.middleware.locale.LocaleMiddleware',/" test_venv/cas/cas/settings.py sed -i 's/from django.conf.urls import url/from django.conf.urls import url, include/' test_venv/cas/cas/urls.py sed -i "s@url(r'^admin/', admin.site.urls),@url(r'^admin/', admin.site.urls),\n url(r'^', include('cas_server.urls', namespace='cas_server')),@" test_venv/cas/cas/urls.py test_venv/bin/python test_venv/cas/manage.py migrate test_venv/bin/python test_venv/cas/manage.py createsuperuser test_venv: test_venv/bin/python test_project: test_venv/cas/manage.py @echo "##############################################################" @echo "A test django project was created in $(realpath test_venv/cas)" run_server: test_project test_venv/bin/python test_venv/cas/manage.py runserver run_tests: test_venv python setup.py check --restructuredtext --stric test_venv/bin/py.test -rw -x --cov=cas_server --cov-report html --cov-report term rm htmlcov/coverage_html.js # I am really pissed off by those keybord shortcuts test_venv/bin/sphinx-build: test_venv test_venv/bin/pip install Sphinx sphinx_rtd_theme docs: test_venv/bin/sphinx-build bash -c "source test_venv/bin/activate; cd docs; make html" publish_pypi_release: python setup.py sdist bdist_wheel upload --sign