<?php
class Shib
{
const SHIB_LOOKUP_JSON = '/var/cache/slx-admin/idp2suffix.json';
const SHIB_DISCO_JSON = '/var/cache/slx-admin/discofeed.json';
const RET_ERR = 0;
const RET_UPDATE = 1;
const RET_UNCHANGED = 2;
/**
* Updates the local copy of the JSON file for mapping IDP entityIDs to scope suffixes.
* If the list changed, all ShibAuth modules will be rebuilt.
*
* @return bool true on success, or if list didn't change. false otherwise.
*/
public static function refreshIdpSuffixMap(): bool
{
$changed = false;
$ret = self::updateFromUrl(CONFIG_SHIB_LOOKUP_URL . '?what=id2suffix', self::SHIB_LOOKUP_JSON);
if ($ret === self::RET_ERR)
return false;
if ($ret === self::RET_UPDATE) {
$changed = true;
}
$ret = self::updateFromUrl(CONFIG_SHIB_DISCO_URL, self::SHIB_DISCO_JSON);
if ($ret === self::RET_ERR)
return false;
if ($ret === self::RET_UPDATE) {
$changed = true;
}
if ($changed) {
// Rebuild any modules in the background
$configs = [];
$list = ConfigModule::getAll('ShibAuth');
$parent = null;
foreach ($list as $mod) {
$r = $mod->generate(false, $parent);
if (is_string($r)) {
$parent = $r;
}
foreach (ConfigTgz::getAllForModule($mod->id()) as $tgz) {
$configs[$tgz->id()] = $tgz;
}
}
foreach ($configs as $tgz) {
$r = $tgz->generate(false, 0, $parent);
if (is_string($r)) {
$parent = $r;
}
}
}
return $ret;
}
private static function updateFromUrl(string $url, string $file): int
{
if (!file_exists($file) || filemtime($file) + 86400 < time()) {
$code = null;
$ret = Download::toFile($file . '.tmp',
$url, 15, $code);
if (!$ret)
return self::RET_ERR;
if (!file_exists($file)
|| filesize($file) !== filesize($file . '.tmp')
|| sha1_file($file, true) !== sha1_file($file . '.tmp', true)
) {
rename($file . '.tmp', $file);
if ($code >= 200 && $code < 300)
return self::RET_UPDATE;
return self::RET_ERR; // Not 2xx range, keep file but return error
}
unlink($file . '.tmp');
}
return self::RET_UNCHANGED;
}
/**
* @return ?array of registrars (key) containing array{"registrar": string, "list": string[]}
*/
public static function getListByRegistrar(): ?array
{
$disco = json_decode(file_get_contents(self::SHIB_DISCO_JSON), true);
$lookup = json_decode(file_get_contents(self::SHIB_LOOKUP_JSON), true);
if (!is_array($disco) || !is_array($lookup)) {
return null;
}
foreach ($disco as $item) {
if (!isset($item['entityID'])) {
continue;
}
$id = $item['entityID'];
$name = null;
$fallback = null;
foreach ($item['DisplayNames'] ?? [] as $dn) {
if ($dn['lang'] === LANG) {
$name = $dn['value'];
} elseif ($name === null && $dn['lang'] === 'en') {
$name = $dn['value'];
} elseif ($fallback === null) {
$fallback = $dn['value'];
}
}
if ($name === null) {
$name = $fallback;
}
$registrar = $lookup[$id]['regauth'] ?? 'unknown';
if (!isset($return[$registrar])) {
$return[$registrar] = ['list' => [], 'registrar' => $registrar];
}
$return[$registrar]['list'][] = [
'id' => $id,
'name' => $name ?? $id,
];
}
return $return;
}
public static function getIdp2SuffixList(): ?array
{
$lookup = json_decode(file_get_contents(self::SHIB_LOOKUP_JSON), true);
if (!is_array($lookup))
return null;
return $lookup;
}
/**
* Map given list of registrar IDs back to all the IdP entityIDs they cover.
* @param string[] $regs
* @return string[]
*/
public static function explodeRegistrars(array $regs): array
{
if (empty($regs))
return [];
$idps = [];
$reg2idps = Shib::getListByRegistrar();
foreach ($regs as $reg) {
if (!isset($reg2idps[$reg]['list']))
continue;
foreach ($reg2idps[$reg]['list'] as $elem) {
$idps[] = $elem['id'];
}
}
return $idps;
}
}