diff --git a/Plugin/unraid/Varaverk.page b/Plugin/unraid/Varaverk.page index ecf8e74..3200b01 100644 --- a/Plugin/unraid/Varaverk.page +++ b/Plugin/unraid/Varaverk.page @@ -24,9 +24,9 @@ unset($_master, $_h1m, $_host1_blank, $_my_hostid, $_conf_missing); // Determine active tab $tab = $_GET['tab'] ?? 'monitor'; -$validTabs = ['monitor', 'scheduler', 'docker', 'watchdog', 'partnership', 'fallback', 'arrs', 'rsync', 'settings']; +$validTabs = ['monitor', 'scheduler', 'docker', 'watchdog', 'partnership', 'fallback', 'arrs', 'rsync', 'auth', 'settings']; if (!in_array($tab, $validTabs)) $tab = 'monitor'; -$tabLabels = ['monitor' => 'Monitor', 'scheduler' => 'Scheduler', 'docker' => 'Docker', 'watchdog' => 'Watchdog', 'partnership' => 'Partnership', 'fallback' => 'FallBack', 'arrs' => 'Arrs', 'rsync' => 'Rsync', 'settings' => 'Settings']; +$tabLabels = ['monitor' => 'Monitor', 'scheduler' => 'Scheduler', 'docker' => 'Docker', 'watchdog' => 'Watchdog', 'partnership' => 'Partnership', 'fallback' => 'FallBack', 'arrs' => 'Arrs', 'rsync' => 'Rsync', 'auth' => 'Auth Stack', 'settings' => 'Settings']; ?> diff --git a/Plugin/unraid/api/auth.php b/Plugin/unraid/api/auth.php new file mode 100644 index 0000000..2dc6023 --- /dev/null +++ b/Plugin/unraid/api/auth.php @@ -0,0 +1,47 @@ + vv_npm_list_proxies(), + 'npm_certs' => ['ok' => true, 'certs' => vv_npm_list_certs()], + 'lldap_users' => vv_lldap_list_users(), + 'lldap_groups' => vv_lldap_list_groups(), + 'authelia_rules' => vv_authelia_read_rules(), + default => ['ok' => false, 'error' => 'Unknown action: ' . $action], + }; + echo json_encode($result); + exit; +} + +if ($_SERVER['REQUEST_METHOD'] !== 'POST') { + http_response_code(405); + echo json_encode(['ok' => false, 'error' => 'GET or POST only']); + exit; +} + +$action = trim($_POST['action'] ?? ''); + +$result = match ($action) { + // NPM + 'npm_create' => vv_npm_create_proxy(json_decode($_POST['data'] ?? '{}', true) ?: []), + 'npm_update' => vv_npm_update_proxy((int)($_POST['id'] ?? 0), json_decode($_POST['data'] ?? '{}', true) ?: []), + 'npm_delete' => vv_npm_delete_proxy((int)($_POST['id'] ?? 0)), + 'npm_toggle' => vv_npm_toggle_proxy((int)($_POST['id'] ?? 0), ($_POST['enabled'] ?? '0') === '1'), + // lldap + 'lldap_create_user' => vv_lldap_create_user($_POST['uid'] ?? '', $_POST['email'] ?? '', $_POST['display_name'] ?? '', $_POST['password'] ?? ''), + 'lldap_update_user' => vv_lldap_update_user($_POST['uid'] ?? '', $_POST['email'] ?? '', $_POST['display_name'] ?? ''), + 'lldap_delete_user' => vv_lldap_delete_user($_POST['uid'] ?? ''), + 'lldap_set_password' => vv_lldap_set_password($_POST['uid'] ?? '', $_POST['password'] ?? ''), + 'lldap_create_group' => vv_lldap_create_group($_POST['name'] ?? ''), + 'lldap_delete_group' => vv_lldap_delete_group((int)($_POST['id'] ?? 0)), + 'lldap_add_to_group' => vv_lldap_add_to_group($_POST['uid'] ?? '', (int)($_POST['gid'] ?? 0)), + 'lldap_remove_from_group' => vv_lldap_remove_from_group($_POST['uid'] ?? '', (int)($_POST['gid'] ?? 0)), + // Authelia + 'authelia_save' => vv_authelia_write_rules(json_decode($_POST['rules'] ?? '[]', true) ?: [], $_POST['default_policy'] ?? 'deny'), + default => ['ok' => false, 'error' => 'Unknown action: ' . $action], +}; + +echo json_encode($result); diff --git a/Plugin/unraid/api/monitor.php b/Plugin/unraid/api/monitor.php index 2c113e6..16406b0 100644 --- a/Plugin/unraid/api/monitor.php +++ b/Plugin/unraid/api/monitor.php @@ -2,9 +2,11 @@ header('Content-Type: application/json'); require_once dirname(__DIR__) . '/include/config.php'; -$_vv_cached = vv_cache_read('monitor', 300); -if ($_vv_cached !== null) { echo json_encode($_vv_cached); exit; } -unset($_vv_cached); +if (!isset($_GET['live'])) { + $_vv_cached = vv_cache_read('monitor', 300); + if ($_vv_cached !== null) { echo json_encode($_vv_cached); exit; } + unset($_vv_cached); +} require_once dirname(__DIR__) . '/include/monitor.php'; require_once dirname(__DIR__) . '/include/vms.php'; diff --git a/Plugin/unraid/include/auth.php b/Plugin/unraid/include/auth.php new file mode 100644 index 0000000..f43d5b0 --- /dev/null +++ b/Plugin/unraid/include/auth.php @@ -0,0 +1,285 @@ + rtrim($v["{$host}_NPM_URL"] ?? 'http://localhost:81', '/'), + 'npm_user' => $v["{$host}_NPM_USER"] ?? '', + 'npm_pass' => $v["{$host}_NPM_PASS"] ?? '', + 'lldap_url' => rtrim($v["{$host}_LLDAP_URL"] ?? 'http://localhost:17170', '/'), + 'lldap_user' => $v["{$host}_LLDAP_USER"] ?? '', + 'lldap_pass' => $v["{$host}_LLDAP_PASS"] ?? '', + 'authelia_config' => $v["{$host}_AUTHELIA_CONFIG"] ?? '/mnt/user/appdata-Fallback/Critical-Data/Authelia/configuration.yml', + 'authelia_container' => $v["{$host}_AUTHELIA_CONTAINER"] ?? 'Authelia', + 'is_owner' => vv_is_owner(), + ]; +} + +// ── NPM ─────────────────────────────────────────────────────────────────────── + +function vv_npm_token(): string { + if (!session_id()) session_start(); + $conf = vv_auth_conf(); + $cached = $_SESSION['vv_npm_token'] ?? ''; + $expiry = $_SESSION['vv_npm_token_exp'] ?? 0; + if ($cached && time() < $expiry) return $cached; + + $resp = vv_npm_raw('POST', '/api/tokens', [ + 'identity' => $conf['npm_user'], + 'secret' => $conf['npm_pass'], + 'expiry' => '1d', + ], '', $conf); + $token = $resp['token'] ?? ''; + if ($token) { + $_SESSION['vv_npm_token'] = $token; + $_SESSION['vv_npm_token_exp'] = time() + 82800; + } + return $token; +} + +function vv_npm_raw(string $method, string $path, array $data, string $token, array $conf = []): array { + if (!$conf) $conf = vv_auth_conf(); + $url = $conf['npm_url'] . $path; + $headers = ['Content-Type: application/json', 'Accept: application/json']; + if ($token) $headers[] = 'Authorization: Bearer ' . $token; + + $ch = curl_init($url); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 10, + CURLOPT_HTTPHEADER => $headers, + CURLOPT_CUSTOMREQUEST => $method, + ]); + if ($data && in_array($method, ['POST', 'PUT'], true)) + curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); + $body = curl_exec($ch); + curl_close($ch); + return json_decode($body ?: '{}', true) ?: []; +} + +function vv_npm_req(string $method, string $path, array $data = []): array { + $token = vv_npm_token(); + if (!$token) return ['_err' => 'NPM auth failed — check credentials in host conf']; + return vv_npm_raw($method, $path, $data, $token); +} + +function vv_npm_list_proxies(): array { + $list = vv_npm_req('GET', '/api/nginx/proxy-hosts?expand=certificate'); + if (!is_array($list) || isset($list['_err'])) + return ['ok' => false, 'error' => $list['_err'] ?? 'Invalid response from NPM']; + return ['ok' => true, 'proxies' => $list]; +} + +function vv_npm_list_certs(): array { + $list = vv_npm_req('GET', '/api/nginx/certificates'); + return is_array($list) ? $list : []; +} + +function vv_npm_create_proxy(array $data): array { + $r = vv_npm_req('POST', '/api/nginx/proxy-hosts', $data); + return isset($r['id']) ? ['ok' => true, 'proxy' => $r] : ['ok' => false, 'error' => $r['error'] ?? ($r['_err'] ?? 'Create failed')]; +} + +function vv_npm_update_proxy(int $id, array $data): array { + $r = vv_npm_req('PUT', "/api/nginx/proxy-hosts/$id", $data); + return isset($r['id']) ? ['ok' => true, 'proxy' => $r] : ['ok' => false, 'error' => $r['error'] ?? ($r['_err'] ?? 'Update failed')]; +} + +function vv_npm_delete_proxy(int $id): array { + vv_npm_req('DELETE', "/api/nginx/proxy-hosts/$id"); + return ['ok' => true]; +} + +function vv_npm_toggle_proxy(int $id, bool $enabled): array { + vv_npm_req('POST', "/api/nginx/proxy-hosts/$id/" . ($enabled ? 'enable' : 'disable')); + return ['ok' => true]; +} + +// ── lldap ───────────────────────────────────────────────────────────────────── + +function vv_lldap_token(): string { + if (!session_id()) session_start(); + $conf = vv_auth_conf(); + $cached = $_SESSION['vv_lldap_token'] ?? ''; + $expiry = $_SESSION['vv_lldap_token_exp'] ?? 0; + if ($cached && time() < $expiry) return $cached; + + $ch = curl_init($conf['lldap_url'] . '/auth/simple/login'); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 10, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => json_encode(['username' => $conf['lldap_user'], 'password' => $conf['lldap_pass']]), + CURLOPT_HTTPHEADER => ['Content-Type: application/json'], + ]); + $body = curl_exec($ch); + curl_close($ch); + $resp = json_decode($body ?: '{}', true) ?: []; + $token = $resp['token'] ?? ''; + if ($token) { + $_SESSION['vv_lldap_token'] = $token; + $_SESSION['vv_lldap_token_exp'] = time() + 3500; + } + return $token; +} + +function vv_lldap_gql(string $query, array $variables = []): array { + $conf = vv_auth_conf(); + $token = vv_lldap_token(); + if (!$token) return ['errors' => [['message' => 'lldap auth failed — check credentials']]]; + + $ch = curl_init($conf['lldap_url'] . '/api/graphql'); + curl_setopt_array($ch, [ + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 10, + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => json_encode(['query' => $query, 'variables' => $variables]), + CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Authorization: Bearer ' . $token], + ]); + $body = curl_exec($ch); + curl_close($ch); + return json_decode($body ?: '{}', true) ?: []; +} + +function vv_lldap_list_users(): array { + $r = vv_lldap_gql('query { listUsers { id displayName email creationDate groups { id displayName } } }'); + if (isset($r['errors'])) return ['ok' => false, 'error' => $r['errors'][0]['message'] ?? 'Query failed']; + return ['ok' => true, 'users' => $r['data']['listUsers'] ?? []]; +} + +function vv_lldap_list_groups(): array { + $r = vv_lldap_gql('query { listGroups { id displayName users { id displayName } } }'); + if (isset($r['errors'])) return ['ok' => false, 'error' => $r['errors'][0]['message'] ?? 'Query failed']; + return ['ok' => true, 'groups' => $r['data']['listGroups'] ?? []]; +} + +function vv_lldap_create_user(string $id, string $email, string $displayName, string $password): array { + $r = vv_lldap_gql( + 'mutation CreateUser($user: CreateUserInput!) { createUser(user: $user) { id displayName email } }', + ['user' => ['id' => $id, 'email' => $email, 'displayName' => $displayName]] + ); + if (isset($r['errors'])) return ['ok' => false, 'error' => $r['errors'][0]['message'] ?? 'Create failed']; + if ($password) vv_lldap_set_password($id, $password); + return ['ok' => true, 'user' => $r['data']['createUser'] ?? []]; +} + +function vv_lldap_update_user(string $id, string $email, string $displayName): array { + $r = vv_lldap_gql( + 'mutation UpdateUser($user: UpdateUserInput!) { updateUser(user: $user) { ok } }', + ['user' => ['id' => $id, 'email' => $email, 'displayName' => $displayName]] + ); + if (isset($r['errors'])) return ['ok' => false, 'error' => $r['errors'][0]['message'] ?? 'Update failed']; + return ['ok' => true]; +} + +function vv_lldap_delete_user(string $id): array { + $r = vv_lldap_gql( + 'mutation DeleteUser($userId: String!) { deleteUser(userId: $userId) { ok } }', + ['userId' => $id] + ); + if (isset($r['errors'])) return ['ok' => false, 'error' => $r['errors'][0]['message'] ?? 'Delete failed']; + return ['ok' => true]; +} + +function vv_lldap_set_password(string $userId, string $password): array { + $r = vv_lldap_gql( + 'mutation ChangePassword($userId: String!, $password: String!) { changeUserPassword(userId: $userId, password: $password) }', + ['userId' => $userId, 'password' => $password] + ); + if (isset($r['errors'])) return ['ok' => false, 'error' => $r['errors'][0]['message'] ?? 'Password change failed']; + return ['ok' => true]; +} + +function vv_lldap_create_group(string $name): array { + $r = vv_lldap_gql( + 'mutation CreateGroup($name: String!) { createGroup(name: $name) { id displayName } }', + ['name' => $name] + ); + if (isset($r['errors'])) return ['ok' => false, 'error' => $r['errors'][0]['message'] ?? 'Create failed']; + return ['ok' => true, 'group' => $r['data']['createGroup'] ?? []]; +} + +function vv_lldap_delete_group(int $id): array { + $r = vv_lldap_gql( + 'mutation DeleteGroup($groupId: Int!) { deleteGroup(groupId: $groupId) { ok } }', + ['groupId' => $id] + ); + if (isset($r['errors'])) return ['ok' => false, 'error' => $r['errors'][0]['message'] ?? 'Delete failed']; + return ['ok' => true]; +} + +function vv_lldap_add_to_group(string $userId, int $groupId): array { + $r = vv_lldap_gql( + 'mutation AddUserToGroup($userId: String!, $groupId: Int!) { addUserToGroup(userId: $userId, groupId: $groupId) { ok } }', + ['userId' => $userId, 'groupId' => $groupId] + ); + if (isset($r['errors'])) return ['ok' => false, 'error' => $r['errors'][0]['message'] ?? 'Failed']; + return ['ok' => true]; +} + +function vv_lldap_remove_from_group(string $userId, int $groupId): array { + $r = vv_lldap_gql( + 'mutation RemoveUserFromGroup($userId: String!, $groupId: Int!) { removeUserFromGroup(userId: $userId, groupId: $groupId) { ok } }', + ['userId' => $userId, 'groupId' => $groupId] + ); + if (isset($r['errors'])) return ['ok' => false, 'error' => $r['errors'][0]['message'] ?? 'Failed']; + return ['ok' => true]; +} + +// ── Authelia ────────────────────────────────────────────────────────────────── + +function vv_authelia_read_rules(): array { + $conf = vv_auth_conf(); + $file = $conf['authelia_config']; + if (!file_exists($file)) return ['ok' => false, 'error' => 'Config not found: ' . $file]; + + $py = "import yaml,json,sys\n" + . "d=yaml.safe_load(open(sys.argv[1]))\n" + . "ac=d.get('access_control',{})\n" + . "print(json.dumps({'default_policy':ac.get('default_policy','deny'),'rules':ac.get('rules',[])}))\n"; + $tmp = '/tmp/vv_auth_rd_' . getmypid() . '.py'; + file_put_contents($tmp, $py); + $out = shell_exec('python3 ' . escapeshellarg($tmp) . ' ' . escapeshellarg($file) . ' 2>/dev/null'); + @unlink($tmp); + + if (!$out) return ['ok' => false, 'error' => 'Parse failed — python3 with PyYAML required']; + $data = json_decode(trim($out), true); + if (!$data) return ['ok' => false, 'error' => 'Invalid YAML response']; + return ['ok' => true, 'default_policy' => $data['default_policy'], 'rules' => $data['rules']]; +} + +function vv_authelia_write_rules(array $rules, string $defaultPolicy): array { + $conf = vv_auth_conf(); + $file = $conf['authelia_config']; + if (!file_exists($file)) return ['ok' => false, 'error' => 'Config not found: ' . $file]; + + $acJson = json_encode(['default_policy' => $defaultPolicy, 'rules' => $rules]); + $py = <<<'PYEOF' +import yaml, json, sys, re +config_file = sys.argv[1] +new_ac = json.loads(sys.argv[2]) +with open(config_file, 'r') as f: + content = f.read() +new_block = yaml.dump({'access_control': new_ac}, default_flow_style=False, allow_unicode=True, sort_keys=False) +pattern = r'(?ms)^access_control:.*?(?=^[a-zA-Z#]|\Z)' +if re.search(pattern, content): + content = re.sub(pattern, new_block + '\n', content) +else: + content = content.rstrip('\n') + '\n\n' + new_block + '\n' +with open(config_file, 'w') as f: + f.write(content) +print('ok') +PYEOF; + $tmp = '/tmp/vv_auth_wr_' . getmypid() . '.py'; + file_put_contents($tmp, $py); + $out = shell_exec('python3 ' . escapeshellarg($tmp) . ' ' . escapeshellarg($file) . ' ' . escapeshellarg($acJson) . ' 2>&1'); + @unlink($tmp); + + if (trim($out) !== 'ok') return ['ok' => false, 'error' => 'Write failed: ' . trim($out)]; + shell_exec('docker restart ' . escapeshellarg($conf['authelia_container']) . ' >/dev/null 2>&1 &'); + return ['ok' => true]; +} diff --git a/Plugin/unraid/pages/auth.php b/Plugin/unraid/pages/auth.php new file mode 100644 index 0000000..7bdcedc --- /dev/null +++ b/Plugin/unraid/pages/auth.php @@ -0,0 +1,917 @@ + + + + +
+ Auth Stack + + + + +
+ + +
+
+ + +
+
+
Loading…
+ + + + + + + +
+
+ + +
+
+ +
+
+ Users + +
+
Loading…
+
+ +
+ +
+
+ Groups + +
+
Loading…
+
+ +
+ +
+
+ + +
+
+ Default policy: + + + default policy editable on HOST1 + + +
+
+ + +
+
+
Loading…
+ + + + + + + + +
+
+ + +
+
+
+ + diff --git a/Plugin/unraid/pages/monitor.php b/Plugin/unraid/pages/monitor.php index f6b48b5..102a737 100644 --- a/Plugin/unraid/pages/monitor.php +++ b/Plugin/unraid/pages/monitor.php @@ -590,8 +590,12 @@ function vvDiskCol(disks) { // ── Poll ────────────────────────────────────────────────────────────────────── +let _vvFirstPoll = true; + function vvPollMonitor() { - fetch('/plugins/varaverk/api/monitor.php') + const _url = _vvFirstPoll ? '/plugins/varaverk/api/monitor.php' : '/plugins/varaverk/api/monitor.php?live=1'; + _vvFirstPoll = false; + fetch(_url) .then(r => r.json()) .then(d => { diff --git a/Plugin/varaverk.plg b/Plugin/varaverk.plg index 147fa89..a6c87eb 100644 --- a/Plugin/varaverk.plg +++ b/Plugin/varaverk.plg @@ -41,19 +41,24 @@ - -https://github.com/FailedProxy/Varaverk/releases/download/&version;/&pkg; -&sha256; + + + +