Files
meetletacbfree/classes/local/sso.php
T

176 lines
6.3 KiB
PHP

<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle 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.
//
// Moodle 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 Moodle. If not, see <http://www.gnu.org/licenses/>.
/**
* Adobe Connect SSO helpers for the current Moodle user.
*
* @package mod_meetletacbfree
* @copyright 2026 MeetLet <info@meetlet.net>
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
*/
namespace mod_meetletacbfree\local;
use mod_meetletacbfree\api\client;
use mod_meetletacbfree\api\service;
use moodle_exception;
use stdClass;
defined('MOODLE_INTERNAL') || die();
/**
* Build Connect logins and BREEZESESSION for Moodle users.
*/
class sso {
/**
* Ordered Connect login candidates for a Moodle user.
*
* @return string[]
*/
public static function login_candidates(stdClass $user, ?stdClass $adobeuser = null): array {
$reserved = [
'administrators', 'authors', 'everyone', 'live-admins', 'admins',
'course-admins', 'event-administrators', 'event-managers', 'learners',
'seminar-administrators',
// Generic Moodle accounts must never be sent as Connect SSO identities.
'admin', 'guest', 'root', 'administrator', 'moodle',
];
$candidates = [];
// Prefer ext-login (refresh_principal_login) then stable mlu{id}, then legacy logins.
$seed = [];
if (!empty($adobeuser->login)) {
$seed[] = (string)$adobeuser->login;
}
if (!empty($adobeuser->internallogin)) {
$seed[] = (string)$adobeuser->internallogin;
}
$seed[] = config::connect_login($user);
foreach (array_merge($seed, config::legacy_connect_logins($user)) as $candidate) {
$candidate = trim((string)$candidate);
if ($candidate === '' || in_array(strtolower($candidate), $reserved, true)) {
continue;
}
if (!in_array($candidate, $candidates, true)) {
$candidates[] = $candidate;
}
}
return $candidates;
}
/**
* Obtain a BREEZESESSION cookie via HTTP header SSO for the given logins.
*
* @param string[] $logincandidates
*/
public static function breeze_session(array $logincandidates): string {
\mod_meetletacbfree\registration\manager::require_registered();
$cookie = '';
$lasterror = null;
$primary = $logincandidates[0] ?? '';
foreach ($logincandidates as $logintry) {
try {
$sso = new client();
$cookie = $sso->login_external($logintry);
if ($cookie !== '') {
return $cookie;
}
} catch (\Throwable $e) {
$lasterror = $e;
}
}
$parsed = $lasterror ? usererror::from_throwable($lasterror) : ['helpid' => 'sso_01', 'facts' => [], 'xml' => ''];
$facts = $parsed['facts'];
if ($primary !== '' && empty($facts['login'])) {
$facts['login'] = $primary;
}
$ip = self::guess_egress_ip();
if ($ip !== '') {
$facts['egress_ip'] = $ip;
}
$helpid = $parsed['helpid'];
if ($lasterror instanceof moodle_exception && $lasterror->errorcode === 'agentrequestfailed') {
$helpid = 'sso_02';
$facts['agent'] = is_scalar($lasterror->a) ? (string)$lasterror->a : $lasterror->getMessage();
}
if ($lasterror instanceof usererror && $lasterror->helpid === 'sso_01') {
$helpid = 'sso_01';
}
throw new usererror($helpid, 'errorhttpauth', $facts, $parsed['xml']);
}
/**
* Best-effort public IP Adobe will see for Moodle API calls.
*/
public static function guess_egress_ip(): string {
try {
$curl = new \curl(['ignoresecurity' => true]);
$curl->setopt(['CURLOPT_TIMEOUT' => 5]);
$raw = $curl->get('https://api.ipify.org');
$ip = trim((string)$raw);
if (preg_match('/^\d{1,3}(?:\.\d{1,3}){3}$/', $ip)) {
return $ip;
}
} catch (\Throwable $e) {
// Ignore.
}
return '';
}
/**
* Ensure Connect user exists, grant permission on a SCO (and parent meeting for
* recordings), then SSO login.
*
* @return array{cookie:string,adobeuser:stdClass}
*/
public static function provision_and_login(stdClass $user, int $scoid, string $permission): array {
\mod_meetletacbfree\registration\manager::require_registered();
$service = new service();
$service->login_admin();
$adobeuser = $service->ensure_user($user);
$adobeuser = $service->refresh_principal_login($adobeuser);
if (trim((string)($adobeuser->login ?? '')) === '') {
throw new usererror('usr_01', 'errorcreateuser', ['login' => config::connect_login($user)]);
}
$targets = [$scoid];
try {
$info = $service->get_client()->request([
'action' => 'sco-info',
'sco-id' => $scoid,
]);
$folder = (int)($info->sco['folder-id'] ?? 0);
// Parent meeting folder ACL is required for recordings and uploaded content.
if ($folder > 0) {
$targets[] = $folder;
}
} catch (\Throwable $e) {
// Best-effort enrichment only.
}
foreach (array_unique($targets) as $aclid) {
if ($aclid > 0) {
$service->assign_permission((int)$adobeuser->principalid, (int)$aclid, $permission);
}
}
$service->logout();
$cookie = self::breeze_session(self::login_candidates($user, $adobeuser));
return ['cookie' => $cookie, 'adobeuser' => $adobeuser];
}
}