From 54cf5a62cab254e923c8d73ae8bd043a1f33c3b1 Mon Sep 17 00:00:00 2001 From: joey Date: Mon, 20 Nov 2006 01:52:18 +0000 Subject: [PATCH] * Make auth methods pluggable. * Move httpauth support to a plugin. * Add an openid plugin to support logging in using OpenID. --- IkiWiki/CGI.pm | 77 +++++++++++++++++++--------- IkiWiki/Plugin/httpauth.pm | 22 ++++++++ IkiWiki/Plugin/openid.pm | 101 +++++++++++++++++++++++++++++++++++++ IkiWiki/Plugin/skeleton.pm | 8 +++ basewiki/style.css | 8 +++ debian/changelog | 8 +++ debian/control | 2 +- doc/features.mdwn | 6 ++- doc/ikiwiki.setup | 4 +- doc/plugins.mdwn | 5 +- doc/plugins/httpauth.mdwn | 9 ++++ doc/plugins/map.mdwn | 1 - doc/plugins/openid.mdwn | 12 +++++ doc/plugins/type/auth.mdwn | 2 + doc/plugins/write.mdwn | 13 +++++ 15 files changed, 245 insertions(+), 33 deletions(-) create mode 100644 IkiWiki/Plugin/httpauth.pm create mode 100644 IkiWiki/Plugin/openid.pm create mode 100644 doc/plugins/httpauth.mdwn create mode 100644 doc/plugins/openid.mdwn create mode 100644 doc/plugins/type/auth.mdwn diff --git a/IkiWiki/CGI.pm b/IkiWiki/CGI.pm index ce4b6ada1..def0549c5 100644 --- a/IkiWiki/CGI.pm +++ b/IkiWiki/CGI.pm @@ -120,7 +120,7 @@ sub cgi_signin ($$) { #{{{ error($@) if $@; my $form = CGI::FormBuilder->new( title => "signin", - fields => [qw(do title page subpage from name password)], + fields => [qw(do title page subpage from name password openid_url)], header => 1, charset => "utf-8", method => 'POST', @@ -149,32 +149,56 @@ sub cgi_signin ($$) { #{{{ $form->field(name => "from", type => "hidden"); $form->field(name => "subpage", type => "hidden"); $form->field(name => "password", type => "password", required => 0); + if ($config{openid}) { + $form->field(name => "openid_url", label => "OpenID", comment => "to log in via OpenID"); + } + else { + $form->field(name => "openid_url", type => "hidden"); + } if ($form->submitted eq "Register" || $form->submitted eq "Create Account") { $form->title("register"); $form->text(""); $form->fields(qw(do title page subpage from name password confirm_password email)); $form->field(name => "confirm_password", type => "password"); $form->field(name => "email", type => "text"); + $form->field(name => "openid_url", type => "hidden"); } if ($q->param("do") ne "signin" && !$form->submitted) { $form->text("You need to log in first."); } if ($form->submitted) { + my $submittype=$form->submitted; + # OpenID login uses the Login button, but validates + # differently. + if ($submittype eq "Login" && $config{openid} && + length $form->field("openid_url")) { + $submittype="OpenID"; + + $form->field( + name => "openid_url", + validate => sub { + # FIXME: ugh + IkiWiki::Plugin::openid::validate($q, $session, $form, shift); + }, + ); + } + # Set required fields based on how form was submitted. my %required=( "Login" => [qw(name password)], "Register" => [], "Create Account" => [qw(name password confirm_password email)], "Mail Password" => [qw(name)], + "OpenID" => [qw(openid_url)], ); - foreach my $opt (@{$required{$form->submitted}}) { + foreach my $opt (@{$required{$submittype}}) { $form->field(name => $opt, required => 1); } # Validate password differently depending on how # form was submitted. - if ($form->submitted eq 'Login') { + if ($submittype eq 'Login') { $form->field( name => "password", validate => sub { @@ -184,13 +208,13 @@ sub cgi_signin ($$) { #{{{ ); $form->field(name => "name", validate => '/^\w+$/'); } - else { + elsif ($submittype ne 'OpenID') { $form->field(name => "password", validate => 'VALUE'); } # And make sure the entered name exists when logging # in or sending email, and does not when registering. - if ($form->submitted eq 'Create Account' || - $form->submitted eq 'Register') { + if ($submittype eq 'Create Account' || + $submittype eq 'Register') { $form->field( name => "name", validate => sub { @@ -201,7 +225,7 @@ sub cgi_signin ($$) { #{{{ }, ); } - else { + elsif ($submittype ne 'OpenID') { $form->field( name => "name", validate => sub { @@ -229,8 +253,8 @@ sub cgi_signin ($$) { #{{{ do => $form->field("do"), page => $form->field("page"), title => $form->field("title"), - subpage => $form->field("subpage"), from => $form->field("from"), + subpage => $form->field("subpage"), )); } else { @@ -680,11 +704,30 @@ sub cgi () { #{{{ { FileName => "$config{wikistatedir}/sessions.db" }); umask($oldmask); + # Auth hooks can sign a user in. + if ($do ne 'signin' && ! defined $session->param("name")) { + run_hooks(auth => sub { + shift->($q, $session) + }); + if (defined $session->param("name")) { + # Make sure whatever user was authed is in the + # userinfo db. + if (! userinfo_get($session->param("name"), "regdate")) { + userinfo_setall($session->param("name"), { + email => "", + password => "", + regdate => time, + }); + } + } + } + # Everything below this point needs the user to be signed in. if (((! $config{anonok} || $do eq 'prefs') && (! $config{httpauth}) && (! defined $session->param("name") || - ! userinfo_get($session->param("name"), "regdate"))) || $do eq 'signin') { + ! userinfo_get($session->param("name"), "regdate"))) + || $do eq 'signin') { cgi_signin($q, $session); # Force session flush with safe umask. @@ -695,22 +738,6 @@ sub cgi () { #{{{ return; } - if ($config{httpauth} && (! defined $session->param("name"))) { - if (! defined $q->remote_user()) { - error("Could not determine authenticated username."); - } - else { - $session->param("name", $q->remote_user()); - if (! userinfo_get($session->param("name"), "regdate")) { - userinfo_setall($session->param("name"), { - email => "", - password => "", - regdate=>time, - }); - } - } - } - if (defined $session->param("name") && userinfo_get($session->param("name"), "banned")) { print $q->header(-status => "403 Forbidden"); $session->delete(); diff --git a/IkiWiki/Plugin/httpauth.pm b/IkiWiki/Plugin/httpauth.pm new file mode 100644 index 000000000..336eb793a --- /dev/null +++ b/IkiWiki/Plugin/httpauth.pm @@ -0,0 +1,22 @@ +#!/usr/bin/perl +# HTTP basic auth plugin. +package IkiWiki::Plugin::httpauth; + +use warnings; +use strict; +use IkiWiki; + +sub import { #{{{ + hook(type => "auth", id => "skeleton", call => \&auth); +} # }}} + +sub auth ($$) { #{{{ + my $cgi=shift; + my $session=shift; + + if (defined $cgi->remote_user()) { + $session->param("name", $cgi->remote_user()); + } +} #}}} + +1 diff --git a/IkiWiki/Plugin/openid.pm b/IkiWiki/Plugin/openid.pm new file mode 100644 index 000000000..55b1c4b17 --- /dev/null +++ b/IkiWiki/Plugin/openid.pm @@ -0,0 +1,101 @@ +#!/usr/bin/perl +# OpenID support. +package IkiWiki::Plugin::openid; + +use warnings; +use strict; +use IkiWiki; + +sub import { #{{{ + hook(type => "checkconfig", id => "smiley", call => \&checkconfig); + hook(type => "auth", id => "skeleton", call => \&auth); +} # }}} + +sub checkconfig () { #{{{ + # Currently part of the OpenID code is in CGI.pm, and is enabled by + # this setting. + # TODO: modularise it all out into this plugin.. + $config{openid}=1; +} #}}} + +sub auth ($$) { #{{{ + my $q=shift; + my $session=shift; + + if (defined $q->param('openid.mode')) { + my $csr=getobj($q, $session); + + if (my $setup_url = $csr->user_setup_url) { + IkiWiki::redirect($q, $setup_url); + } + elsif ($csr->user_cancel) { + IkiWiki::redirect($q, $config{url}); + } + elsif (my $vident = $csr->verified_identity) { + $session->param(name => $vident->url); + } + } +} #}}} + +sub validate ($$$$) { #{{{ + my $q=shift; + my $session=shift; + my $form=shift; + my $openid_url=shift; + + my $csr=getobj($q, $session); + + my $claimed_identity = $csr->claimed_identity($openid_url); + if (! $claimed_identity) { + # Put the error in the form and fail validation. + $form->field(name => "openid_url", comment => $csr->err); + return 0; + } + my $check_url = $claimed_identity->check_url( + return_to => IkiWiki::cgiurl( + do => $form->field("do"), + page => $form->field("page"), + title => $form->field("title"), + from => $form->field("from"), + subpage => $form->field("subpage") + ), + trust_root => $config{cgiurl}, + delayed_return => 1, + ); + # Redirect the user to the OpenID server, which will + # eventually bounce them back to auth() above. + IkiWiki::redirect($q, $check_url); + exit 0; +} #}}} + +sub getobj ($$) { #{{{ + my $q=shift; + my $session=shift; + + eval q{use Net::OpenID::Consumer}; + error($@) if $@; + + my $ua; + eval q{use LWPx::ParanoidAgent}; + if (! $@) { + $ua=LWPx::ParanoidAgent->new; + } + else { + $ua=LWP::UserAgent->new; + } + + # Store the secret in the session. + my $secret=$session->param("openid_secret"); + if (! defined $secret) { + $secret=$session->param(openid_secret => time); + } + + return Net::OpenID::Consumer->new( + ua => $ua, + args => $q, + consumer_secret => $secret, + required_root => $config{cgiurl}, + ); +} #}}} + +1 diff --git a/IkiWiki/Plugin/skeleton.pm b/IkiWiki/Plugin/skeleton.pm index acac16c1a..f3244ae14 100644 --- a/IkiWiki/Plugin/skeleton.pm +++ b/IkiWiki/Plugin/skeleton.pm @@ -20,6 +20,7 @@ sub import { #{{{ hook(type => "delete", id => "skeleton", call => \&delete); hook(type => "change", id => "skeleton", call => \&change); hook(type => "cgi", id => "skeleton", call => \&cgi); + hook(type => "auth", id => "skeleton", call => \&auth); hook(type => "savestate", id => "savestate", call => \&savestate); } # }}} @@ -95,6 +96,13 @@ sub cgi ($) { #{{{ debug("skeleton plugin running in cgi"); } #}}} +sub auth ($$) { #{{{ + my $cgi=shift; + my $session=shift; + + debug("skeleton plugin running in auth"); +} #}}} + sub savestate () { #{{{ debug("skeleton plugin running in savestate"); } #}}} diff --git a/basewiki/style.css b/basewiki/style.css index 0334670e1..032665e70 100644 --- a/basewiki/style.css +++ b/basewiki/style.css @@ -205,3 +205,11 @@ li.L7 { li.L8 { list-style: upper-alpha; } + +input#openid_url { + background: url(http://openid.net/login-bg.gif) no-repeat; + background-color: #fff; + background-position: 0 50%; + color: #000; + padding-left: 18px; +} diff --git a/debian/changelog b/debian/changelog index afa0d9228..f261f92c9 100644 --- a/debian/changelog +++ b/debian/changelog @@ -1,3 +1,11 @@ +ikiwiki (1.34) UNRELEASED; urgency=low + + * Make auth methods pluggable. + * Move httpauth support to a plugin. + * Add an openid plugin to support logging in using OpenID. + + -- Joey Hess Sun, 19 Nov 2006 16:40:26 -0500 + ikiwiki (1.33) unstable; urgency=low * Fix issue with aggregate plugin updating expired pages. diff --git a/debian/control b/debian/control index 074a2922e..710e24185 100644 --- a/debian/control +++ b/debian/control @@ -11,7 +11,7 @@ Package: ikiwiki Architecture: all Depends: ${perl:Depends}, libxml-simple-perl, markdown, libtimedate-perl, libhtml-template-perl, libhtml-scrubber-perl, libcgi-formbuilder-perl (>= 3.02.02), libtime-duration-perl, libcgi-session-perl (>= 4.14-1), libmail-sendmail-perl, gcc | c-compiler, libc6-dev | libc-dev, libhtml-parser-perl, liburi-perl Recommends: subversion | git-core | tla | mercurial, hyperestraier -Suggests: viewcvs, librpc-xml-perl, libtext-wikiformat-perl, python-docutils, polygen, tidy, libxml-feed-perl, libmailtools-perl, perlmagick, libfile-mimeinfo-perl +Suggests: viewcvs, librpc-xml-perl, libtext-wikiformat-perl, python-docutils, polygen, tidy, libxml-feed-perl, libmailtools-perl, perlmagick, libfile-mimeinfo-perl, libnet-openid-consumer-perl, libcrypt-ssleay-perl Description: a wiki compiler ikiwiki converts a directory full of wiki pages into html pages suitable for publishing on a website. Unlike many wikis, ikiwiki does not have its diff --git a/doc/features.mdwn b/doc/features.mdwn index cb118f27d..05aeebfc9 100644 --- a/doc/features.mdwn +++ b/doc/features.mdwn @@ -129,7 +129,11 @@ and can be enabled by enabling [[CGI]]. ### User registration Can optionally be configured to allow only registered users to post -pages; online user registration form, etc. +pages. + +User registration can be done using a web form, or ikiwiki can be +configured to accept users authenticated with OpenID, or HTTP basic +authentication, or other methods implemented via plugins. ### Discussion pages diff --git a/doc/ikiwiki.setup b/doc/ikiwiki.setup index 7cff65c5b..6eb6446e0 100644 --- a/doc/ikiwiki.setup +++ b/doc/ikiwiki.setup @@ -88,8 +88,6 @@ use IkiWiki::Setup::Standard { #locale => 'en_US.UTF-8', # Only send cookies over SSL connections. #sslcookie => 1, - # Use HTTP Authentication instead of Ikiwiki's. - #httpauth => 1, # Logging settings: verbose => 0, syslog => 0, @@ -97,7 +95,7 @@ use IkiWiki::Setup::Standard { # To add plugins, list them here. #add_plugins => [qw{meta tag pagecount brokenlinks search smiley # wikitext camelcase pagestats htmltidy fortune - # sidebar map rst toc linkmap}], + # sidebar map rst toc linkmap openid}], # If you want to disable any of the default plugins, list them here. #disable_plugins => [qw{inline htmlscrubber}], diff --git a/doc/plugins.mdwn b/doc/plugins.mdwn index 45e380d58..6de17adc7 100644 --- a/doc/plugins.mdwn +++ b/doc/plugins.mdwn @@ -1,7 +1,8 @@ Most of ikiwiki's [[features]] are implemented as plugins. Beyond the [[type/core]] features, there are plugins to [[type/format]] text, -use [[type/tags]], show [[type/meta]] information, do other [[type/useful]] -stuff, add [[type/chrome]] to the wiki, or just have [[type/fun]]. +use [[type/tags]], show [[type/meta]] information, add [[type/auth]] +methods, do other [[type/useful]] stuff, add [[type/chrome]] to the +wiki, or just have [[type/fun]]. There's documentation if you want to [[write]] your own plugins, or you can install and use plugins contributed by others. diff --git a/doc/plugins/httpauth.mdwn b/doc/plugins/httpauth.mdwn new file mode 100644 index 000000000..98233570c --- /dev/null +++ b/doc/plugins/httpauth.mdwn @@ -0,0 +1,9 @@ +[[template id=plugin name=httpauth included=1 author="Alec Berryman"]] +[[tag type/auth]] + +This plugin allows HTTP basic authentication to be used to log into the +wiki. To use the plugin, your web server should be set up to perform HTTP +basic authentiation. The authenticated user will be automatically signed +into the wiki. + +This plugin is included in ikiwiki, but is not enabled by default. diff --git a/doc/plugins/map.mdwn b/doc/plugins/map.mdwn index 01f61e5b3..592a20706 100644 --- a/doc/plugins/map.mdwn +++ b/doc/plugins/map.mdwn @@ -12,7 +12,6 @@ Hint: To limit the map to displaying pages less than a certian level deep, use a [[PageSpec]] like this: `pages="* and !*/*/*"` This plugin is included in ikiwiki, but is not enabled by default. -It was contributed by Alessandro Dotti Contra. If this plugin is enabled, here is a page map for the plugins section of this wiki: diff --git a/doc/plugins/openid.mdwn b/doc/plugins/openid.mdwn new file mode 100644 index 000000000..344be7de3 --- /dev/null +++ b/doc/plugins/openid.mdwn @@ -0,0 +1,12 @@ +[[template id=plugin name=openid included=1 author="[[Joey]]"]] +[[tag type/auth]] + +This plugin allows users to use their [OpenID](http://openid.net/) to log +into the wiki. + +The plugin needs the `Net::OpenID::Consumer` perl module. The +`LWPx::ParanoidAgent` perl module is used if available, for added +security. Finally, the `Crypt::SSLeay` perl module is needed to support +users entering "https" OpenID urls. + +This plugin is included in ikiwiki, but is not enabled by default. diff --git a/doc/plugins/type/auth.mdwn b/doc/plugins/type/auth.mdwn new file mode 100644 index 000000000..a6ae5e4ea --- /dev/null +++ b/doc/plugins/type/auth.mdwn @@ -0,0 +1,2 @@ +These plugins add different authentication methods for logging in to the +wiki. diff --git a/doc/plugins/write.mdwn b/doc/plugins/write.mdwn index 5cace0911..7c4da8d5f 100644 --- a/doc/plugins/write.mdwn +++ b/doc/plugins/write.mdwn @@ -175,6 +175,19 @@ called in turn, and passed a CGI object. The hook should examine the parameters, and if it will handle this CGI request, output a page and terminate the program. +### cgi + + hook(type => "cgi", id => "foo", call => \&auth); + +This hook can be used to implement a different authentication method than +the standard web form. When a user needs to be authenticated, each registered +auth hook is called in turn, and passed a CGI object and a session object. + +If the hook is able to authenticate the user, it should set the session +object's "name" parameter to the authenticated user's name. Note that +if the name is set to the name of a user who is not registered, +a basic registration of the user will be automatically performed. + ### savestate hook(type => "savestate", id => "foo", call => \&savestate); -- 2.32.0.93.g670b81a890