Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * Class for loading/storing oauth2 endpoints from the DB.
19
 *
20
 * @package    core
21
 * @copyright  2017 Damyon Wiese
22
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
namespace core\oauth2;
25
 
26
defined('MOODLE_INTERNAL') || die();
27
 
28
require_once($CFG->libdir . '/filelib.php');
29
 
30
use stdClass;
31
use moodle_url;
32
use context_system;
33
use moodle_exception;
34
 
35
/**
36
 * Static list of api methods for system oauth2 configuration.
37
 *
38
 * @copyright  2017 Damyon Wiese
39
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40
 */
41
class api {
42
 
43
    /**
44
     * Initializes a record for one of the standard issuers to be displayed in the settings.
45
     * The issuer is not yet created in the database.
46
     * @param string $type One of google, facebook, microsoft, nextcloud, imsobv2p1
47
     * @return \core\oauth2\issuer
48
     */
49
    public static function init_standard_issuer($type) {
50
        require_capability('moodle/site:config', context_system::instance());
51
 
52
        $classname = self::get_service_classname($type);
53
        if (class_exists($classname)) {
54
            return $classname::init();
55
        }
56
        throw new moodle_exception('OAuth 2 service type not recognised: ' . $type);
57
    }
58
 
59
    /**
60
     * Create endpoints for standard issuers, based on the issuer created from submitted data.
61
     * @param string $type One of google, facebook, microsoft, nextcloud, imsobv2p1
62
     * @param issuer $issuer issuer the endpoints should be created for.
63
     * @return \core\oauth2\issuer
64
     */
65
    public static function create_endpoints_for_standard_issuer($type, $issuer) {
66
        require_capability('moodle/site:config', context_system::instance());
67
 
68
        $classname = self::get_service_classname($type);
69
        if (class_exists($classname)) {
70
            $classname::create_endpoints($issuer);
71
            return $issuer;
72
        }
73
        throw new moodle_exception('OAuth 2 service type not recognised: ' . $type);
74
    }
75
 
76
    /**
77
     * Create one of the standard issuers.
78
     *
79
     * @param string $type One of google, facebook, microsoft, MoodleNet, nextcloud or imsobv2p1
80
     * @param string|false $baseurl Baseurl (only required for nextcloud, imsobv2p1 and moodlenet)
81
     * @return \core\oauth2\issuer
82
     */
83
    public static function create_standard_issuer($type, $baseurl = false) {
84
        require_capability('moodle/site:config', context_system::instance());
85
 
86
        switch ($type) {
87
            case 'imsobv2p1':
88
                if (!$baseurl) {
89
                    throw new moodle_exception('IMS OBv2.1 service type requires the baseurl parameter.');
90
                }
91
            case 'nextcloud':
92
                if (!$baseurl) {
93
                    throw new moodle_exception('Nextcloud service type requires the baseurl parameter.');
94
                }
95
            case 'moodlenet':
96
                if (!$baseurl) {
97
                    throw new moodle_exception('MoodleNet service type requires the baseurl parameter.');
98
                }
99
            case 'google':
100
            case 'facebook':
101
            case 'microsoft':
102
                $classname = self::get_service_classname($type);
103
                $issuer = $classname::init();
104
                if ($baseurl) {
105
                    $issuer->set('baseurl', $baseurl);
106
                }
107
                $issuer->create();
108
                return self::create_endpoints_for_standard_issuer($type, $issuer);
109
        }
110
 
111
        throw new moodle_exception('OAuth 2 service type not recognised: ' . $type);
112
    }
113
 
114
 
115
    /**
116
     * List all the issuers, ordered by the sortorder field
117
     *
118
     * @param bool $includeloginonly also include issuers that are configured to be shown only on login page,
119
     *     By default false, in this case the method returns all issuers that can be used in services
120
     * @return \core\oauth2\issuer[]
121
     */
122
    public static function get_all_issuers(bool $includeloginonly = false) {
123
        if ($includeloginonly) {
124
            return issuer::get_records([], 'sortorder');
125
        } else {
126
            return array_values(issuer::get_records_select('showonloginpage<>?', [issuer::LOGINONLY], 'sortorder'));
127
        }
128
    }
129
 
130
    /**
131
     * Get a single issuer by id.
132
     *
133
     * @param int $id
134
     * @return \core\oauth2\issuer
135
     */
136
    public static function get_issuer($id) {
137
        return new issuer($id);
138
    }
139
 
140
    /**
141
     * Get a single endpoint by id.
142
     *
143
     * @param int $id
144
     * @return \core\oauth2\endpoint
145
     */
146
    public static function get_endpoint($id) {
147
        return new endpoint($id);
148
    }
149
 
150
    /**
151
     * Get a single user field mapping by id.
152
     *
153
     * @param int $id
154
     * @return \core\oauth2\user_field_mapping
155
     */
156
    public static function get_user_field_mapping($id) {
157
        return new user_field_mapping($id);
158
    }
159
 
160
    /**
161
     * Get the system account for an installed OAuth service.
162
     * Never ever ever expose this to a webservice because it contains the refresh token which grants API access.
163
     *
164
     * @param \core\oauth2\issuer $issuer
165
     * @return system_account|false
166
     */
167
    public static function get_system_account(issuer $issuer) {
168
        return system_account::get_record(['issuerid' => $issuer->get('id')]);
169
    }
170
 
171
    /**
172
     * Get the full list of system scopes required by an oauth issuer.
173
     * This includes the list required for login as well as any scopes injected by the oauth2_system_scopes callback in plugins.
174
     *
175
     * @param \core\oauth2\issuer $issuer
176
     * @return string
177
     */
178
    public static function get_system_scopes_for_issuer($issuer) {
179
        $scopes = $issuer->get('loginscopesoffline');
180
 
181
        $pluginsfunction = get_plugins_with_function('oauth2_system_scopes', 'lib.php');
182
        foreach ($pluginsfunction as $plugintype => $plugins) {
183
            foreach ($plugins as $pluginfunction) {
184
                // Get additional scopes from the plugin.
185
                $pluginscopes = $pluginfunction($issuer);
186
                if (empty($pluginscopes)) {
187
                    continue;
188
                }
189
 
190
                // Merge the additional scopes with the existing ones.
191
                $additionalscopes = explode(' ', $pluginscopes);
192
 
193
                foreach ($additionalscopes as $scope) {
194
                    if (!empty($scope)) {
195
                        if (strpos(' ' . $scopes . ' ', ' ' . $scope . ' ') === false) {
196
                            $scopes .= ' ' . $scope;
197
                        }
198
                    }
199
                }
200
            }
201
        }
202
 
203
        return $scopes;
204
    }
205
 
206
    /**
207
     * Get an authenticated oauth2 client using the system account.
208
     * This call uses the refresh token to get an access token.
209
     *
210
     * @param \core\oauth2\issuer $issuer
211
     * @return \core\oauth2\client|false An authenticated client (or false if the token could not be upgraded)
212
     * @throws moodle_exception Request for token upgrade failed for technical reasons
213
     */
214
    public static function get_system_oauth_client(issuer $issuer) {
215
        $systemaccount = self::get_system_account($issuer);
216
        if (empty($systemaccount)) {
217
            return false;
218
        }
219
        // Get all the scopes!
220
        $scopes = self::get_system_scopes_for_issuer($issuer);
221
        $class = self::get_client_classname($issuer->get('servicetype'));
222
        $client = new $class($issuer, null, $scopes, true);
223
 
224
        if (!$client->is_logged_in()) {
225
            if (!$client->upgrade_refresh_token($systemaccount)) {
226
                return false;
227
            }
228
        }
229
        return $client;
230
    }
231
 
232
    /**
233
     * Get an authenticated oauth2 client using the current user account.
234
     * This call does the redirect dance back to the current page after authentication.
235
     *
236
     * @param \core\oauth2\issuer $issuer The desired OAuth issuer
237
     * @param moodle_url $currenturl The url to the current page.
238
     * @param string $additionalscopes The additional scopes required for authorization.
239
     * @param bool $autorefresh Should the client support the use of refresh tokens to persist access across sessions.
240
     * @return \core\oauth2\client
241
     */
242
    public static function get_user_oauth_client(issuer $issuer, moodle_url $currenturl, $additionalscopes = '',
243
            $autorefresh = false) {
244
        $class = self::get_client_classname($issuer->get('servicetype'));
245
        $client = new $class($issuer, $currenturl, $additionalscopes, false, $autorefresh);
246
 
247
        return $client;
248
    }
249
 
250
    /**
251
     * Get the client classname for an issuer.
252
     *
253
     * @param string $type The OAuth issuer type (google, facebook...).
254
     * @return string The classname for the custom client or core client class if the class for the defined type
255
     *                 doesn't exist or null type is defined.
256
     */
257
    protected static function get_client_classname(?string $type): string {
258
        // Default core client class.
259
        $classname = 'core\\oauth2\\client';
260
 
261
        if (!empty($type)) {
262
            $typeclassname = 'core\\oauth2\\client\\' . $type;
263
            if (class_exists($typeclassname)) {
264
                $classname = $typeclassname;
265
            }
266
        }
267
 
268
        return $classname;
269
    }
270
 
271
    /**
272
     * Get the list of defined endpoints for this OAuth issuer
273
     *
274
     * @param \core\oauth2\issuer $issuer The desired OAuth issuer
275
     * @return \core\oauth2\endpoint[]
276
     */
277
    public static function get_endpoints(issuer $issuer) {
278
        return endpoint::get_records(['issuerid' => $issuer->get('id')]);
279
    }
280
 
281
    /**
282
     * Get the list of defined mapping from OAuth user fields to moodle user fields.
283
     *
284
     * @param \core\oauth2\issuer $issuer The desired OAuth issuer
285
     * @return \core\oauth2\user_field_mapping[]
286
     */
287
    public static function get_user_field_mappings(issuer $issuer) {
288
        return user_field_mapping::get_records(['issuerid' => $issuer->get('id')]);
289
    }
290
 
291
    /**
292
     * Guess an image from the discovery URL.
293
     *
294
     * @param \core\oauth2\issuer $issuer The desired OAuth issuer
295
     */
296
    protected static function guess_image($issuer) {
297
        if (empty($issuer->get('image')) && !empty($issuer->get('baseurl'))) {
298
            $baseurl = parse_url($issuer->get('baseurl'));
299
            $imageurl = $baseurl['scheme'] . '://' . $baseurl['host'] . '/favicon.ico';
300
            $issuer->set('image', $imageurl);
301
            $issuer->update();
302
        }
303
    }
304
 
305
    /**
306
     * Take the data from the mform and update the issuer.
307
     *
308
     * @param stdClass $data
309
     * @return \core\oauth2\issuer
310
     */
311
    public static function update_issuer($data) {
312
        return self::create_or_update_issuer($data, false);
313
    }
314
 
315
    /**
316
     * Take the data from the mform and create the issuer.
317
     *
318
     * @param stdClass $data
319
     * @return \core\oauth2\issuer
320
     */
321
    public static function create_issuer($data) {
322
        return self::create_or_update_issuer($data, true);
323
    }
324
 
325
    /**
326
     * Take the data from the mform and create or update the issuer.
327
     *
328
     * @param stdClass $data Form data for them issuer to be created/updated.
329
     * @param bool $create If true, the issuer will be created; otherwise, it will be updated.
330
     * @return issuer The created/updated issuer.
331
     */
332
    protected static function create_or_update_issuer($data, bool $create): issuer {
333
        require_capability('moodle/site:config', context_system::instance());
334
        $issuer = new issuer($data->id ?? 0, $data);
335
 
336
        // Will throw exceptions on validation failures.
337
        if ($create) {
338
            $issuer->create();
339
 
340
            // Perform service discovery.
341
            $classname = self::get_service_classname($issuer->get('servicetype'));
342
            $classname::discover_endpoints($issuer);
343
            self::guess_image($issuer);
344
        } else {
345
            $issuer->update();
346
        }
347
 
348
        return $issuer;
349
    }
350
 
351
    /**
352
     * Get the service classname for an issuer.
353
     *
354
     * @param string $type The OAuth issuer type (google, facebook...).
355
     *
356
     * @return string The classname for this issuer or "Custom" service class if the class for the defined type doesn't exist
357
     *                 or null type is defined.
358
     */
359
    protected static function get_service_classname(?string $type): string {
360
        // Default custom service class.
361
        $classname = 'core\\oauth2\\service\\custom';
362
 
363
        if (!empty($type)) {
364
            $typeclassname = 'core\\oauth2\\service\\' . $type;
365
            if (class_exists($typeclassname)) {
366
                $classname = $typeclassname;
367
            }
368
        }
369
 
370
        return $classname;
371
    }
372
 
373
    /**
374
     * Take the data from the mform and update the endpoint.
375
     *
376
     * @param stdClass $data
377
     * @return \core\oauth2\endpoint
378
     */
379
    public static function update_endpoint($data) {
380
        require_capability('moodle/site:config', context_system::instance());
381
        $endpoint = new endpoint(0, $data);
382
 
383
        // Will throw exceptions on validation failures.
384
        $endpoint->update();
385
 
386
        return $endpoint;
387
    }
388
 
389
    /**
390
     * Take the data from the mform and create the endpoint.
391
     *
392
     * @param stdClass $data
393
     * @return \core\oauth2\endpoint
394
     */
395
    public static function create_endpoint($data) {
396
        require_capability('moodle/site:config', context_system::instance());
397
        $endpoint = new endpoint(0, $data);
398
 
399
        // Will throw exceptions on validation failures.
400
        $endpoint->create();
401
        return $endpoint;
402
    }
403
 
404
    /**
405
     * Take the data from the mform and update the user field mapping.
406
     *
407
     * @param stdClass $data
408
     * @return \core\oauth2\user_field_mapping
409
     */
410
    public static function update_user_field_mapping($data) {
411
        require_capability('moodle/site:config', context_system::instance());
412
        $userfieldmapping = new user_field_mapping(0, $data);
413
 
414
        // Will throw exceptions on validation failures.
415
        $userfieldmapping->update();
416
 
417
        return $userfieldmapping;
418
    }
419
 
420
    /**
421
     * Take the data from the mform and create the user field mapping.
422
     *
423
     * @param stdClass $data
424
     * @return \core\oauth2\user_field_mapping
425
     */
426
    public static function create_user_field_mapping($data) {
427
        require_capability('moodle/site:config', context_system::instance());
428
        $userfieldmapping = new user_field_mapping(0, $data);
429
 
430
        // Will throw exceptions on validation failures.
431
        $userfieldmapping->create();
432
        return $userfieldmapping;
433
    }
434
 
435
    /**
436
     * Reorder this identity issuer.
437
     *
438
     * Requires moodle/site:config capability at the system context.
439
     *
440
     * @param int $id The id of the identity issuer to move.
441
     * @return boolean
442
     */
443
    public static function move_up_issuer($id) {
444
        require_capability('moodle/site:config', context_system::instance());
445
        $current = new issuer($id);
446
 
447
        $sortorder = $current->get('sortorder');
448
        if ($sortorder == 0) {
449
            return false;
450
        }
451
 
452
        $sortorder = $sortorder - 1;
453
        $current->set('sortorder', $sortorder);
454
 
455
        $filters = array('sortorder' => $sortorder);
456
        $children = issuer::get_records($filters, 'id');
457
        foreach ($children as $needtoswap) {
458
            $needtoswap->set('sortorder', $sortorder + 1);
459
            $needtoswap->update();
460
        }
461
 
462
        // OK - all set.
463
        $result = $current->update();
464
 
465
        return $result;
466
    }
467
 
468
    /**
469
     * Reorder this identity issuer.
470
     *
471
     * Requires moodle/site:config capability at the system context.
472
     *
473
     * @param int $id The id of the identity issuer to move.
474
     * @return boolean
475
     */
476
    public static function move_down_issuer($id) {
477
        require_capability('moodle/site:config', context_system::instance());
478
        $current = new issuer($id);
479
 
480
        $max = issuer::count_records();
481
        if ($max > 0) {
482
            $max--;
483
        }
484
 
485
        $sortorder = $current->get('sortorder');
486
        if ($sortorder >= $max) {
487
            return false;
488
        }
489
        $sortorder = $sortorder + 1;
490
        $current->set('sortorder', $sortorder);
491
 
492
        $filters = array('sortorder' => $sortorder);
493
        $children = issuer::get_records($filters);
494
        foreach ($children as $needtoswap) {
495
            $needtoswap->set('sortorder', $sortorder - 1);
496
            $needtoswap->update();
497
        }
498
 
499
        // OK - all set.
500
        $result = $current->update();
501
 
502
        return $result;
503
    }
504
 
505
    /**
506
     * Disable an identity issuer.
507
     *
508
     * Requires moodle/site:config capability at the system context.
509
     *
510
     * @param int $id The id of the identity issuer to disable.
511
     * @return boolean
512
     */
513
    public static function disable_issuer($id) {
514
        require_capability('moodle/site:config', context_system::instance());
515
        $issuer = new issuer($id);
516
 
517
        $issuer->set('enabled', 0);
518
        return $issuer->update();
519
    }
520
 
521
 
522
    /**
523
     * Enable an identity issuer.
524
     *
525
     * Requires moodle/site:config capability at the system context.
526
     *
527
     * @param int $id The id of the identity issuer to enable.
528
     * @return boolean
529
     */
530
    public static function enable_issuer($id) {
531
        require_capability('moodle/site:config', context_system::instance());
532
        $issuer = new issuer($id);
533
 
534
        $issuer->set('enabled', 1);
535
        return $issuer->update();
536
    }
537
 
538
    /**
539
     * Delete an identity issuer.
540
     *
541
     * Requires moodle/site:config capability at the system context.
542
     *
543
     * @param int $id The id of the identity issuer to delete.
544
     * @return boolean
545
     */
546
    public static function delete_issuer($id) {
547
        require_capability('moodle/site:config', context_system::instance());
548
        $issuer = new issuer($id);
549
 
550
        $systemaccount = self::get_system_account($issuer);
551
        if ($systemaccount) {
552
            $systemaccount->delete();
553
        }
554
        $endpoints = self::get_endpoints($issuer);
555
        if ($endpoints) {
556
            foreach ($endpoints as $endpoint) {
557
                $endpoint->delete();
558
            }
559
        }
560
 
561
        // Will throw exceptions on validation failures.
562
        return $issuer->delete();
563
    }
564
 
565
    /**
566
     * Delete an endpoint.
567
     *
568
     * Requires moodle/site:config capability at the system context.
569
     *
570
     * @param int $id The id of the endpoint to delete.
571
     * @return boolean
572
     */
573
    public static function delete_endpoint($id) {
574
        require_capability('moodle/site:config', context_system::instance());
575
        $endpoint = new endpoint($id);
576
 
577
        // Will throw exceptions on validation failures.
578
        return $endpoint->delete();
579
    }
580
 
581
    /**
582
     * Delete a user_field_mapping.
583
     *
584
     * Requires moodle/site:config capability at the system context.
585
     *
586
     * @param int $id The id of the user_field_mapping to delete.
587
     * @return boolean
588
     */
589
    public static function delete_user_field_mapping($id) {
590
        require_capability('moodle/site:config', context_system::instance());
591
        $userfieldmapping = new user_field_mapping($id);
592
 
593
        // Will throw exceptions on validation failures.
594
        return $userfieldmapping->delete();
595
    }
596
 
597
    /**
598
     * Perform the OAuth dance and get a refresh token.
599
     *
600
     * Requires moodle/site:config capability at the system context.
601
     *
602
     * @param \core\oauth2\issuer $issuer
603
     * @param moodle_url $returnurl The url to the current page (we will be redirected back here after authentication).
604
     * @return boolean
605
     */
606
    public static function connect_system_account($issuer, $returnurl) {
607
        require_capability('moodle/site:config', context_system::instance());
608
 
609
        // We need to authenticate with an oauth 2 client AS a system user and get a refresh token for offline access.
610
        $scopes = self::get_system_scopes_for_issuer($issuer);
611
 
612
        // Allow callbacks to inject non-standard scopes to the auth request.
613
        $class = self::get_client_classname($issuer->get('servicetype'));
614
        $client = new $class($issuer, $returnurl, $scopes, true);
615
 
616
        if (!optional_param('response', false, PARAM_BOOL)) {
617
            $client->log_out();
618
        }
619
 
620
        if (optional_param('error', '', PARAM_RAW)) {
621
            return false;
622
        }
623
 
624
        if (!$client->is_logged_in()) {
625
            redirect($client->get_login_url());
626
        }
627
 
628
        $refreshtoken = $client->get_refresh_token();
629
        if (!$refreshtoken) {
630
            return false;
631
        }
632
 
633
        $systemaccount = self::get_system_account($issuer);
634
        if ($systemaccount) {
635
            $systemaccount->delete();
636
        }
637
 
638
        $userinfo = $client->get_userinfo();
639
 
640
        $record = new stdClass();
641
        $record->issuerid = $issuer->get('id');
642
        $record->refreshtoken = $refreshtoken;
643
        $record->grantedscopes = $scopes;
644
        $record->email = isset($userinfo['email']) ? $userinfo['email'] : '';
645
        $record->username = $userinfo['username'];
646
 
647
        $systemaccount = new system_account(0, $record);
648
 
649
        $systemaccount->create();
650
 
651
        $client->log_out();
652
        return true;
653
    }
654
}