afc45db81b64f05ecc2467ceaecc5e2cf2bc75db
[freeside.git] / FS / FS / part_export / acct_google.pm
1 package FS::part_export::acct_google;
2
3 use strict;
4 use vars qw(%info %SIG $CACHE);
5 use Tie::IxHash;
6
7 use base 'FS::part_export';
8
9 tie my %options, 'Tie::IxHash',
10   'domain'    => { label => 'Domain name' },
11   'username'  => { label => 'Admin username' },
12   'password'  => { label => 'Admin password' },
13 ;
14 # To handle multiple domains, use separate instances of 
15 # the export.  We assume that they all have different 
16 # admin logins.
17
18 %info = (
19   'svc'       => 'svc_acct',
20   'desc'      => 'Google hosted mail',
21   'options'   => \%options,
22   'nodomain'  => 'Y',
23   'notes'    => <<'END'
24 Export accounts to the Google Provisioning API.  Requires 
25 REST::Google::Apps::Provisioning from CPAN.
26 END
27 );
28
29 sub rebless { shift; }
30
31 sub _export_insert {
32   my($self, $svc_acct) = (shift, shift);
33   $svc_acct->finger =~ /^(.*)\s+(\S+)$/;
34   my ($first, $last) = ($1, $2);
35   $self->google_request('createUser',
36     'username'      => $svc_acct->username,
37     'password'      => $svc_acct->_password,
38     'givenName'     => $first,
39     'familyName'    => $last,
40   );
41 }
42
43 sub _export_replace {
44   my( $self, $new, $old ) = (shift, shift, shift);
45   # We have to do this in two steps, so do the renameUser last so that 
46   # if it fails partway through the username is still coherent.
47   if ( $new->_password ne $old->_password
48     or $new->finger    ne $old->finger ) {
49     $new->finger =~ /^(.*)\s+(\S+)$/;
50     my ($first, $last) = ($1, $2);
51     my $error = $self->google_request('updateUser',
52       'username'    => $old->username,
53       'password'    => $new->_password,
54       'givenName'   => $first,
55       'familyName'  => $last,
56     );
57     return $error if $error;
58   }
59   if ( $new->username ne $old->username ) {
60     my $error = $self->google_request('renameUser',
61       'username'  => $old->username,
62       'newname'   => $new->username
63     );
64     return $error if $error;
65   }
66   return;
67 }
68
69 sub _export_delete {
70   my( $self, $svc_acct ) = (shift, shift);
71   $self->google_request('deleteUser',
72     'username'  => $svc_acct->username
73   );
74 }
75
76 sub _export_suspend {
77   my( $self, $svc_acct ) = (shift, shift);
78   $self->google_request('updateUser',
79     'username'  => $svc_acct->username,
80     'suspended' => 'true',
81   );
82 }
83
84 sub _export_unsuspend {
85   my( $self, $svc_acct ) = (shift, shift);
86   $self->google_request('updateUser',
87     'username'  => $svc_acct->username,
88     'suspended' => 'false',
89   );
90 }
91
92 sub auth_error {
93   my $self = shift;
94   my $google = $self->google_handle;
95   if ( $google->{'error'} ) {
96     my $url = $google->{'captcha_url'} || '';
97     $url = "http://www.google.com/accounts/$url" if $url;
98     return { 'captcha_url' => $url,
99              'message'     => 
100                'Unable to connect to the Google API: '.$google->{'error'}.'.',
101            };
102   }
103   return; #nothing on success
104 }
105
106 sub captcha_auth {
107   my $self = shift;
108   my $response = shift;
109   my $google = $self->google_handle('captcha_response' => $response);
110   return (defined($google->{'token'}));
111 }
112
113 my %google_error = (
114   1000 => 'unknown error',
115   1001 => 'server busy',
116   1100 => 'username belongs to a recently deleted account',
117   1101 => 'user suspended',
118   1200 => 'domain user limit exceeded',
119   1201 => 'domain alias limit exceeded',
120   1202 => 'domain suspended',
121   1203 => 'feature not available on this domain',
122   1300 => 'username in use',
123   1301 => 'user not found',
124   1302 => 'reserved username',
125   1400 => 'illegal character in first name',
126   1401 => 'illegal character in last name',
127   1402 => 'invalid password',
128   1403 => 'illegal character in username',
129   # should be everything we need
130 );
131
132 # Runs the request and returns nothing if it succeeds, or an 
133 # error message.
134
135 sub google_request {
136   my ($self, $method, %opt) = @_;
137   my $google = $self->google_handle(
138     'captcha_response' => delete $opt{'captcha_response'}
139   );
140   return $google->{'error'} if $google->{'error'};
141
142   # Throw away the result from this; we don't use it yet.
143   eval { $google->$method(%opt) };
144   if ( $@ ) {
145     return $google_error{ $@->{'error'}->{'errorCode'} } || $@->{'error'};
146   }
147   return;
148 }
149
150 # Returns a REST::Google::Apps::Provisioning object which is hooked 
151 # to die {error => stuff} on API errors.  The cached auth token 
152 # will be used if possible.  If not, try to authenticate.  On 
153 # authentication error, the R:G:A:P object will still be returned 
154 # but with $google->{'error'} set to the error message.
155
156 sub google_handle {
157   my $self = shift;
158   my %opt = @_;
159   my @class = ( 
160     'REST::Google::Apps::Provisioning',
161     'Cache::FileCache',
162     'LWP::UserAgent 5.815',
163   );
164   foreach (@class) {
165     eval "use $_";
166     die "failed to load $_\n" if $@;
167   }
168   $CACHE ||= new Cache::FileCache( {
169       'namespace'   => __PACKAGE__,
170       'cache_root'  => "$FS::UID::cache_dir/cache.$FS::UID::datasrc",
171   } );
172   my $google = REST::Google::Apps::Provisioning->new(
173     'domain'  => $self->option('domain') 
174   );
175
176   # REST::Google::Apps::Provisioning lacks error reporting.  We deal 
177   # with that by hooking HTTP::Response to throw a useful fatal error 
178   # on failure.
179   $google->{'lwp'}->add_handler( 'response_done' =>
180     sub {
181       my $response = shift;
182       return if $response->is_success;
183
184       my $error = '';
185       if ( $response->content =~ /^</ ) {
186         #presume xml
187         $error = $google->{'xml'}->parse_string($response->content);
188       }
189       elsif ( $response->content =~ /=/ ) {
190         $error = +{ map { if ( /^(\w+)=(.*)$/ ) { lc($1) => $2 } }
191           split("\n", $response->content)
192         };
193       }
194       else { # have something to say if there is no response...
195         $error = {'error' => $response->status_line};
196       }
197       die $error;
198     }
199   );
200
201   my $cache_token = $self->exportnum . '_token';
202   my $cache_captcha = $self->exportnum . '_captcha_token';
203   $google->{'token'} = $CACHE->get($cache_token);
204   if ( !$google->{'token'} ) {
205     my %login = (
206       'username' => $self->option('username'),
207       'password' => $self->option('password'),
208     );
209     if ( $opt{'captcha_response'} ) {
210       $login{'logincaptcha'} = $opt{'captcha_response'};
211       $login{'logintoken'} = $CACHE->get($cache_captcha);
212     }
213     eval { $google->captcha_auth(%login); };
214     if ( $@ ) {
215       $google->{'error'} = $@->{'error'};
216       $google->{'captcha_url'} = $@->{'captchaurl'};
217       $CACHE->set($cache_captcha, $@->{'captchatoken'}, '1 minute');
218       return $google;
219     }
220     $CACHE->remove($cache_captcha);
221     $CACHE->set($cache_token, $google->{'token'}, '1 hour');
222   }
223   return $google;
224 }
225
226 # REST::Google::Apps::Provisioning also lacks a way to do this
227 sub REST::Google::Apps::Provisioning::captcha_auth {
228   my $self = shift;
229
230   return( 1 ) if $self->{'token'};
231
232   my ( $arg );
233   %{$arg} = @_;
234
235   map { $arg->{lc($_)} = $arg->{$_} } keys %{$arg};
236
237   foreach my $param ( qw/ username password / ) {
238     $arg->{$param} || croak( "Missing required '$param' argument" );
239   }
240
241   my @postargs = (
242     'accountType' => 'HOSTED',
243     'service'     => 'apps',
244     'Email'       => $arg->{'username'} . '@' . $self->{'domain'},
245     'Passwd'      => $arg->{'password'},
246   );
247   if ( $arg->{'logincaptcha'} ) {
248     push @postargs, 
249       'logintoken'  => $arg->{'logintoken'},
250       'logincaptcha'=> $arg->{'logincaptcha'}
251       ;
252   }
253   my $response = $self->{'lwp'}->post(
254     'https://www.google.com/accounts/ClientLogin',
255     \@postargs
256   );
257
258   $response->is_success() || return( 0 );
259
260   foreach ( split( /\n/, $response->content() ) ) {
261     $self->{'token'} = $1 if /^Auth=(.+)$/;
262     last if $self->{'token'};
263   }
264
265   return( 1 ) if $self->{'token'} || return( 0 );
266 }
267
268 1;