package FS::part_export::acct_google; use strict; use vars qw(%info %SIG $CACHE); use Tie::IxHash; use base 'FS::part_export'; tie my %options, 'Tie::IxHash', 'domain' => { label => 'Domain name' }, 'username' => { label => 'Admin username' }, 'password' => { label => 'Admin password' }, ; # To handle multiple domains, use separate instances of # the export. We assume that they all have different # admin logins. %info = ( 'svc' => 'svc_acct', 'desc' => 'Google hosted mail', 'options' => \%options, 'nodomain' => 'Y', 'no_machine' => 1, 'default_svc_class' => 'Email', 'notes' => <<'END' Export accounts to the Google Provisioning API. Requires REST::Google::Apps::Provisioning from CPAN. END ); sub rebless { shift; } sub _export_insert { my($self, $svc_acct) = (shift, shift); $svc_acct->finger =~ /^(.*)\s+(\S+)$/; my ($first, $last) = ($1, $2); $self->google_request('createUser', 'username' => $svc_acct->username, 'password' => $svc_acct->_password, 'givenName' => $first, 'familyName' => $last, ); } sub _export_replace { my( $self, $new, $old ) = (shift, shift, shift); # We have to do this in two steps, so do the renameUser last so that # if it fails partway through the username is still coherent. if ( $new->_password ne $old->_password or $new->finger ne $old->finger ) { $new->finger =~ /^(.*)\s+(\S+)$/; my ($first, $last) = ($1, $2); my $error = $self->google_request('updateUser', 'username' => $old->username, 'password' => $new->_password, 'givenName' => $first, 'familyName' => $last, ); return $error if $error; } if ( $new->username ne $old->username ) { my $error = $self->google_request('renameUser', 'username' => $old->username, 'newname' => $new->username ); return $error if $error; } return; } sub _export_delete { my( $self, $svc_acct ) = (shift, shift); $self->google_request('deleteUser', 'username' => $svc_acct->username ); } sub _export_suspend { my( $self, $svc_acct ) = (shift, shift); $self->google_request('updateUser', 'username' => $svc_acct->username, 'suspended' => 'true', ); } sub _export_unsuspend { my( $self, $svc_acct ) = (shift, shift); $self->google_request('updateUser', 'username' => $svc_acct->username, 'suspended' => 'false', ); } sub auth_error { my $self = shift; my $google = $self->google_handle; if ( $google->{'error'} ) { my $url = $google->{'captcha_url'} || ''; $url = "http://www.google.com/accounts/$url" if $url; return { 'captcha_url' => $url, 'message' => 'Unable to connect to the Google API: '.$google->{'error'}.'.', }; } return; #nothing on success } sub captcha_auth { my $self = shift; my $response = shift; my $google = $self->google_handle('captcha_response' => $response); return (defined($google->{'token'})); } my %google_error = ( 1000 => 'unknown error', 1001 => 'server busy', 1100 => 'username belongs to a recently deleted account', 1101 => 'user suspended', 1200 => 'domain user limit exceeded', 1201 => 'domain alias limit exceeded', 1202 => 'domain suspended', 1203 => 'feature not available on this domain', 1300 => 'username in use', 1301 => 'user not found', 1302 => 'reserved username', 1400 => 'illegal character in first name', 1401 => 'illegal character in last name', 1402 => 'invalid password', 1403 => 'illegal character in username', # should be everything we need ); # Runs the request and returns nothing if it succeeds, or an # error message. sub google_request { my ($self, $method, %opt) = @_; my $google = $self->google_handle( 'captcha_response' => delete $opt{'captcha_response'} ); return $google->{'error'} if $google->{'error'}; # Throw away the result from this; we don't use it yet. eval { $google->$method(%opt) }; if ( $@ ) { return $google_error{ $@->{'error'}->{'errorCode'} } || $@->{'error'}; } return; } # Returns a REST::Google::Apps::Provisioning object which is hooked # to die {error => stuff} on API errors. The cached auth token # will be used if possible. If not, try to authenticate. On # authentication error, the R:G:A:P object will still be returned # but with $google->{'error'} set to the error message. sub google_handle { my $self = shift; my %opt = @_; my @class = ( 'REST::Google::Apps::Provisioning', 'Cache::FileCache', 'LWP::UserAgent 5.815', ); foreach (@class) { eval "use $_"; die "failed to load $_\n" if $@; } $CACHE ||= new Cache::FileCache( { 'namespace' => __PACKAGE__, 'cache_root' => "$FS::UID::cache_dir/cache.$FS::UID::datasrc", } ); my $google = REST::Google::Apps::Provisioning->new( 'domain' => $self->option('domain') ); # REST::Google::Apps::Provisioning lacks error reporting. We deal # with that by hooking HTTP::Response to throw a useful fatal error # on failure. $google->{'lwp'}->add_handler( 'response_done' => sub { my $response = shift; return if $response->is_success; my $error = ''; if ( $response->content =~ /^{'xml'}->parse_string($response->content); } elsif ( $response->content =~ /=/ ) { $error = +{ map { if ( /^(\w+)=(.*)$/ ) { lc($1) => $2 } } split("\n", $response->content) }; } else { # have something to say if there is no response... $error = {'error' => $response->status_line}; } die $error; } ); my $cache_token = $self->exportnum . '_token'; my $cache_captcha = $self->exportnum . '_captcha_token'; $google->{'token'} = $CACHE->get($cache_token); if ( !$google->{'token'} ) { my %login = ( 'username' => $self->option('username'), 'password' => $self->option('password'), ); if ( $opt{'captcha_response'} ) { $login{'logincaptcha'} = $opt{'captcha_response'}; $login{'logintoken'} = $CACHE->get($cache_captcha); } eval { $google->captcha_auth(%login); }; if ( $@ ) { $google->{'error'} = $@->{'error'}; $google->{'captcha_url'} = $@->{'captchaurl'}; $CACHE->set($cache_captcha, $@->{'captchatoken'}, '1 minute'); return $google; } $CACHE->remove($cache_captcha); $CACHE->set($cache_token, $google->{'token'}, '1 hour'); } return $google; } # REST::Google::Apps::Provisioning also lacks a way to do this sub REST::Google::Apps::Provisioning::captcha_auth { my $self = shift; return( 1 ) if $self->{'token'}; my ( $arg ); %{$arg} = @_; map { $arg->{lc($_)} = $arg->{$_} } keys %{$arg}; foreach my $param ( qw/ username password / ) { $arg->{$param} || croak( "Missing required '$param' argument" ); } my @postargs = ( 'accountType' => 'HOSTED', 'service' => 'apps', 'Email' => $arg->{'username'} . '@' . $self->{'domain'}, 'Passwd' => $arg->{'password'}, ); if ( $arg->{'logincaptcha'} ) { push @postargs, 'logintoken' => $arg->{'logintoken'}, 'logincaptcha'=> $arg->{'logincaptcha'} ; } my $response = $self->{'lwp'}->post( 'https://www.google.com/accounts/ClientLogin', \@postargs ); $response->is_success() || return( 0 ); foreach ( split( /\n/, $response->content() ) ) { $self->{'token'} = $1 if /^Auth=(.+)$/; last if $self->{'token'}; } return( 1 ) if $self->{'token'} || return( 0 ); } 1;