| 1: | <?php |
| 2: | declare(strict_types=1); |
| 3: | |
| 4: | |
| 5: | |
| 6: | |
| 7: | |
| 8: | |
| 9: | |
| 10: | |
| 11: | |
| 12: | |
| 13: | |
| 14: | |
| 15: | use Auth\UI\SecureAccessKey; |
| 16: | use Module\Skeleton\Contracts\Hookable; |
| 17: | use Module\Skeleton\Contracts\Tasking; |
| 18: | use Module\Support\Auth; |
| 19: | use Opcenter\Account\State; |
| 20: | use Opcenter\Auth\Password; |
| 21: | use Opcenter\Auth\Shadow; |
| 22: | use Opcenter\Mail\Services\Dovecot; |
| 23: | |
| 24: | |
| 25: | |
| 26: | |
| 27: | |
| 28: | |
| 29: | class Auth_Module extends Auth implements Hookable, Tasking |
| 30: | { |
| 31: | const DEPENDENCY_MAP = [ |
| 32: | 'siteinfo', |
| 33: | 'users' |
| 34: | ]; |
| 35: | |
| 36: | const API_KEY_LIMIT = 10; |
| 37: | const API_USER_SYNC_COMMENT = PANEL_BRAND . ' user sync'; |
| 38: | |
| 39: | const PWOVERRIDE_KEY = 'pwoverride'; |
| 40: | |
| 41: | const SECURITY_TOKEN = \Auth\Sectoken::SECURITY_TOKEN; |
| 42: | |
| 43: | const MIN_PW_LENGTH = AUTH_MIN_PW_LENGTH; |
| 44: | |
| 45: | const PAM_SERVICES = ['cp', 'dav']; |
| 46: | |
| 47: | private static $domain_db; |
| 48: | |
| 49: | protected $exportedFunctions = [ |
| 50: | '*' => PRIVILEGE_ALL, |
| 51: | 'totp_disable' => PRIVILEGE_ALL | PRIVILEGE_EXTAUTH, |
| 52: | 'inactive_reason' => PRIVILEGE_SITE|PRIVILEGE_USER|PRIVILEGE_RESELLER, |
| 53: | 'verify_password' => PRIVILEGE_SERVER_EXEC | PRIVILEGE_ALL, |
| 54: | 'verify_totp' => PRIVILEGE_SERVER_EXEC | PRIVILEGE_ALL, |
| 55: | 'change_domain' => PRIVILEGE_SITE, |
| 56: | 'change_username' => PRIVILEGE_SITE | PRIVILEGE_ADMIN, |
| 57: | 'reset_password' => PRIVILEGE_SITE | PRIVILEGE_ADMIN, |
| 58: | 'set_temp_password' => PRIVILEGE_ADMIN | PRIVILEGE_SITE |
| 59: | ]; |
| 60: | |
| 61: | |
| 62: | |
| 63: | public function __construct() |
| 64: | { |
| 65: | parent::__construct(); |
| 66: | if (!AUTH_ALLOW_USERNAME_CHANGE) { |
| 67: | $this->exportedFunctions['change_username'] = PRIVILEGE_ADMIN; |
| 68: | } |
| 69: | if (!AUTH_ALLOW_DOMAIN_CHANGE) { |
| 70: | $this->exportedFunctions['change_domain'] = PRIVILEGE_NONE; |
| 71: | } |
| 72: | |
| 73: | if (!posix_geteuid()) { |
| 74: | |
| 75: | $this->exportedFunctions['totp_disable'] ^= PRIVILEGE_EXTAUTH; |
| 76: | } |
| 77: | } |
| 78: | |
| 79: | |
| 80: | |
| 81: | |
| 82: | |
| 83: | |
| 84: | public function session_info(): array |
| 85: | { |
| 86: | return (array)$this->getAuthContext(); |
| 87: | } |
| 88: | |
| 89: | |
| 90: | |
| 91: | |
| 92: | |
| 93: | |
| 94: | |
| 95: | |
| 96: | |
| 97: | |
| 98: | |
| 99: | |
| 100: | |
| 101: | public function change_password(string $password, string $user = null, string $domain = null): bool |
| 102: | { |
| 103: | if (!$this->password_permitted($password, $user)) { |
| 104: | return error('weak password disallowed'); |
| 105: | } else if ($this->is_demo()) { |
| 106: | return error('cannot change password in demo mode'); |
| 107: | } |
| 108: | $crypted = $this->crypt($password); |
| 109: | |
| 110: | return $this->change_cpassword($crypted, $user, $domain); |
| 111: | } |
| 112: | |
| 113: | |
| 114: | |
| 115: | |
| 116: | |
| 117: | |
| 118: | |
| 119: | |
| 120: | public function reset_password(?string $user = null, string $domain = null): ?string |
| 121: | { |
| 122: | if (!IS_CLI) { |
| 123: | return $this->query('auth_reset_password', $user, $domain); |
| 124: | } |
| 125: | |
| 126: | if ($this->is_demo()) { |
| 127: | return nerror('cannot change password in demo mode'); |
| 128: | } |
| 129: | |
| 130: | if ($this->permission_level & PRIVILEGE_SITE) { |
| 131: | $domain = $this->domain; |
| 132: | } else if ($domain) { |
| 133: | if (null === ($tmp = \Auth::get_site_id_from_anything($domain))) { |
| 134: | return nerror("Cannot find `%s'", $domain); |
| 135: | } |
| 136: | $domain = \Auth::get_domain_from_site_id($tmp); |
| 137: | } |
| 138: | |
| 139: | if (!$user && !$domain) { |
| 140: | $user = $this->username; |
| 141: | } else if ($domain && !$user) { |
| 142: | if (null === ($tmp = \Auth::get_admin_from_site_id(\Auth::get_site_id_from_anything($domain)))) { |
| 143: | return nerror("Cannot find admin for `%s'", $domain); |
| 144: | } |
| 145: | $user = $tmp; |
| 146: | } |
| 147: | |
| 148: | $password = Password::generate(max(self::MIN_PW_LENGTH, 8)); |
| 149: | |
| 150: | if (!$this->setCryptedPassword($this->crypt($password), $user, $domain)) { |
| 151: | return null; |
| 152: | } |
| 153: | |
| 154: | $ctx = \Auth::context($user, $domain); |
| 155: | $email = \apnscpFunctionInterceptor::factory($ctx)->common_get_email(); |
| 156: | |
| 157: | success("Password set to: `%s'", $password); |
| 158: | |
| 159: | $this->sendNotice( |
| 160: | 'password', |
| 161: | [ |
| 162: | 'email' => $email, |
| 163: | 'password' => $password, |
| 164: | 'username' => $user, |
| 165: | 'domain' => $domain |
| 166: | ] |
| 167: | ); |
| 168: | |
| 169: | \apnscpSession::invalidate_by_user($this->site_id, $user, true); |
| 170: | if ($domain) { |
| 171: | $afi = $this->permission_level & PRIVILEGE_ADMIN ? \apnscpFunctionInterceptor::factory($ctx) : $this; |
| 172: | $afi->site_kill_user($user) && (!Dovecot::exists() || Dovecot::flushAuth()); |
| 173: | } |
| 174: | |
| 175: | return $password; |
| 176: | } |
| 177: | |
| 178: | |
| 179: | |
| 180: | |
| 181: | |
| 182: | |
| 183: | |
| 184: | |
| 185: | public function password_permitted(string $password, string $user = null): bool |
| 186: | { |
| 187: | return Password::strong($password, $user); |
| 188: | } |
| 189: | |
| 190: | |
| 191: | |
| 192: | |
| 193: | |
| 194: | |
| 195: | public function is_demo(): bool |
| 196: | { |
| 197: | |
| 198: | |
| 199: | |
| 200: | if ($this->permission_level & PRIVILEGE_ADMIN) { |
| 201: | return DEMO_ADMIN_LOCK; |
| 202: | } |
| 203: | |
| 204: | return $this->billing_get_invoice() == DEMO_INVOICE || $this->getConfig('billing', 'parent_invoice') === DEMO_INVOICE; |
| 205: | } |
| 206: | |
| 207: | |
| 208: | |
| 209: | |
| 210: | |
| 211: | |
| 212: | |
| 213: | |
| 214: | public function crypt(string $password, string $salt = null): string |
| 215: | { |
| 216: | return Shadow::crypt($password, $salt); |
| 217: | } |
| 218: | |
| 219: | |
| 220: | |
| 221: | |
| 222: | |
| 223: | |
| 224: | |
| 225: | |
| 226: | |
| 227: | |
| 228: | |
| 229: | |
| 230: | |
| 231: | |
| 232: | |
| 233: | |
| 234: | |
| 235: | public function change_cpassword(string $cpassword, string $user = null, string $domain = null): bool |
| 236: | { |
| 237: | if ($this->is_demo()) { |
| 238: | return error('demo account password changes disabled'); |
| 239: | } |
| 240: | |
| 241: | $user = $user ?? $this->username; |
| 242: | $domain = $domain ?: $this->domain; |
| 243: | |
| 244: | if (!IS_CLI) { |
| 245: | $ret = $this->query('auth_change_cpassword', $cpassword, $user, $domain); |
| 246: | if (!$ret) { |
| 247: | return $ret; |
| 248: | } |
| 249: | if ($this->permission_level & (PRIVILEGE_SITE | PRIVILEGE_USER)) { |
| 250: | if ($this->getServiceValue(self::getAuthService(), self::PWOVERRIDE_KEY)) { |
| 251: | return true; |
| 252: | } |
| 253: | |
| 254: | if ($user !== $this->username) { |
| 255: | $email = \apnscpFunctionInterceptor::factory(\Auth::context($user, $this->site))->common_get_email(); |
| 256: | } |
| 257: | $email = $email ?? $this->common_get_email() ?? $this->getConfig('siteinfo', 'email'); |
| 258: | } else if ($this->permission_level & PRIVILEGE_ADMIN) { |
| 259: | if (!$domain) { |
| 260: | $email = $this->common_get_email(); |
| 261: | } else { |
| 262: | |
| 263: | $afi = \apnscpFunctionInterceptor::factory(\Auth::context(null, $domain)); |
| 264: | if ($afi->common_get_service_value(self::getAuthService(), self::PWOVERRIDE_KEY)) { |
| 265: | |
| 266: | return true; |
| 267: | } |
| 268: | $email = $afi->common_get_email(); |
| 269: | } |
| 270: | } |
| 271: | parent::sendNotice( |
| 272: | 'password', |
| 273: | [ |
| 274: | 'email' => $email, |
| 275: | 'ip' => \Auth::client_ip(), |
| 276: | 'username' => $user, |
| 277: | 'domain' => $domain ?: $this->domain |
| 278: | ] |
| 279: | ); |
| 280: | \apnscpSession::invalidate_by_user($this->site_id, $user, true); |
| 281: | |
| 282: | return $ret; |
| 283: | } |
| 284: | |
| 285: | return $this->setCryptedPassword($cpassword, $user, $domain); |
| 286: | } |
| 287: | |
| 288: | |
| 289: | |
| 290: | |
| 291: | |
| 292: | |
| 293: | public function is_inactive(): bool |
| 294: | { |
| 295: | if (!IS_CLI) { |
| 296: | return $this->query('auth_is_inactive'); |
| 297: | } |
| 298: | if ($this->permission_level & (PRIVILEGE_USER | PRIVILEGE_SITE)) { |
| 299: | return file_exists(State::disableMarker($this->site)); |
| 300: | } |
| 301: | |
| 302: | if ($this->permission_level & PRIVILEGE_RESELLER) { |
| 303: | $query = "SELECT 1 FROM resellers WHERE username = '" . $this->username . "' AND enabled = 'f'"; |
| 304: | |
| 305: | } |
| 306: | |
| 307: | return false; |
| 308: | } |
| 309: | |
| 310: | |
| 311: | |
| 312: | |
| 313: | |
| 314: | |
| 315: | public function inactive_reason(): ?string |
| 316: | { |
| 317: | if (!IS_CLI) { |
| 318: | return $this->query('auth_inactive_reason'); |
| 319: | } |
| 320: | |
| 321: | if ($this->permission_level & PRIVILEGE_USER || |
| 322: | !AUTH_SHOW_SUSPENSION_REASON || !$this->is_inactive()) { |
| 323: | return null; |
| 324: | } |
| 325: | |
| 326: | if (!file_exists($path = State::disableMarker($this->site))) { |
| 327: | return null; |
| 328: | } |
| 329: | |
| 330: | return rtrim(implode("\n", array_filter( |
| 331: | file($path, FILE_IGNORE_NEW_LINES), |
| 332: | static function ($line) { |
| 333: | $firstChar = ltrim($line)[0] ?? ''; |
| 334: | return $firstChar !== '#' && $firstChar !== ';'; |
| 335: | }) |
| 336: | )); |
| 337: | } |
| 338: | |
| 339: | |
| 340: | |
| 341: | |
| 342: | |
| 343: | |
| 344: | |
| 345: | |
| 346: | |
| 347: | |
| 348: | |
| 349: | |
| 350: | |
| 351: | |
| 352: | |
| 353: | |
| 354: | public function create_api_key(string $comment = '', string $user = null): ?string |
| 355: | { |
| 356: | if (!$user || !($this->permission_level & PRIVILEGE_SITE)) { |
| 357: | $user = $this->username; |
| 358: | } else if (!$this->user_exists($user)) { |
| 359: | error("cannot set comment for key, user `%s' does not exist", $user); |
| 360: | return null; |
| 361: | } |
| 362: | |
| 363: | if (strlen($comment) > 255) { |
| 364: | warn('api key comment truncated beyond 255 characters'); |
| 365: | } |
| 366: | $key = hash('sha256', uniqid((string)random_int(PHP_INT_MIN, PHP_INT_MAX), true)); |
| 367: | $invoice = null; |
| 368: | if (!($this->permission_level & PRIVILEGE_ADMIN)) { |
| 369: | $invoice = $this->billing_get_invoice(); |
| 370: | if (!$invoice) { |
| 371: | error('unable to find invoice for account'); |
| 372: | return null; |
| 373: | } |
| 374: | } |
| 375: | $db = Auth_SOAP::get_api_db(); |
| 376: | $qfrag = $this->_getAPIQueryFragment(); |
| 377: | $rs = $db->query('SELECT |
| 378: | `api_key` |
| 379: | FROM `api_keys` ' . |
| 380: | $qfrag['join'] . |
| 381: | "WHERE |
| 382: | `username` = '" . $user . "' |
| 383: | AND " . $qfrag['where'] . ' GROUP BY (api_key)'); |
| 384: | |
| 385: | if ((!$this->permission_level & PRIVILEGE_ADMIN) && ($rs->num_rows >= self::API_KEY_LIMIT)) { |
| 386: | error('%d key limit reached', self::API_KEY_LIMIT); |
| 387: | return null; |
| 388: | } |
| 389: | $q = 'INSERT INTO `api_keys` ' . |
| 390: | '(`api_key`, `server_name`, `username`, `site_id`, `invoice`)' . |
| 391: | "VALUES (?,'" . SERVER_NAME_SHORT . "',?,?,?)"; |
| 392: | $stmt = $db->prepare($q); |
| 393: | if ($this->permission_level & PRIVILEGE_ADMIN) { |
| 394: | $site_id = null; |
| 395: | $invoice = null; |
| 396: | } else if ($this->permission_level & PRIVILEGE_RESELLER) { |
| 397: | $site_id = null; |
| 398: | $invoice = $this->billing_get_invoice(); |
| 399: | } else { |
| 400: | $site_id = $this->site_id; |
| 401: | $invoice = $this->billing_get_invoice(); |
| 402: | } |
| 403: | $stmt->bind_param('ssds', $key, $this->username, $site_id, $invoice); |
| 404: | if (!$stmt->execute()) { |
| 405: | error('unable to add key - %s', $stmt->error); |
| 406: | return null; |
| 407: | } |
| 408: | if ($comment) { |
| 409: | $this->set_api_key_comment($key, $comment, $user); |
| 410: | } |
| 411: | |
| 412: | return $key; |
| 413: | } |
| 414: | |
| 415: | |
| 416: | |
| 417: | |
| 418: | |
| 419: | |
| 420: | private function _getAPIQueryFragment(): array |
| 421: | { |
| 422: | $qfrag = array('where' => '1 = 1', 'join' => ''); |
| 423: | if ($this->permission_level & PRIVILEGE_ADMIN) { |
| 424: | $qfrag['where'] = 'api_keys.invoice IS NULL AND site_id IS NULL'; |
| 425: | } else { |
| 426: | $invoice = $this->billing_get_invoice(); |
| 427: | if (!$invoice) { |
| 428: | error('cannot get billing invoice for API key'); |
| 429: | $qfrag['where'] = '1 = 0'; |
| 430: | |
| 431: | return $qfrag; |
| 432: | } |
| 433: | $qfrag['where'] = "api_keys.invoice = '" . Auth_SOAP::get_api_db()->real_escape_string($invoice) . "'"; |
| 434: | } |
| 435: | |
| 436: | return $qfrag; |
| 437: | } |
| 438: | |
| 439: | |
| 440: | |
| 441: | |
| 442: | |
| 443: | |
| 444: | |
| 445: | |
| 446: | |
| 447: | |
| 448: | public function set_api_key_comment(string $key, string $comment = null, string $user = null): bool |
| 449: | { |
| 450: | $key = str_replace('-', '', strtolower($key)); |
| 451: | if (!ctype_xdigit($key)) { |
| 452: | return error($key . ': invalid key'); |
| 453: | } |
| 454: | |
| 455: | |
| 456: | if (strlen($comment) > 255) { |
| 457: | warn('comment truncated to max length 255 characters'); |
| 458: | } |
| 459: | if (!$user || !($this->permission_level & PRIVILEGE_SITE)) { |
| 460: | $user = $this->username; |
| 461: | } else if (!$this->user_exists($user)) { |
| 462: | return error("cannot set comment for key, user `%s' does not exist", $user); |
| 463: | } |
| 464: | $db = Auth_SOAP::get_api_db(); |
| 465: | $qfrag = $this->_getAPIQueryFragment(); |
| 466: | $rs = $db->query('UPDATE `api_keys` ' . $qfrag['join'] . |
| 467: | "SET comment = '" . $db->escape_string($comment) . "' |
| 468: | WHERE `api_key` = '" . strtolower($key) . "' |
| 469: | AND " . $qfrag['where'] . " |
| 470: | AND `username` = '" . $user . "';"); |
| 471: | |
| 472: | return $rs && $db->affected_rows > 0; |
| 473: | } |
| 474: | |
| 475: | |
| 476: | |
| 477: | |
| 478: | |
| 479: | |
| 480: | |
| 481: | |
| 482: | |
| 483: | |
| 484: | |
| 485: | |
| 486: | public function verify_password(#[SensitiveParameter] string $password): bool |
| 487: | { |
| 488: | $file = static::ADMIN_AUTH; |
| 489: | if ($this->permission_level & (PRIVILEGE_SITE | PRIVILEGE_USER)) { |
| 490: | if (!$this->site) { |
| 491: | return false; |
| 492: | } |
| 493: | $file = $this->domain_fs_path('/etc/shadow'); |
| 494: | } |
| 495: | $fp = fopen($file, 'r'); |
| 496: | if (!$fp) { |
| 497: | return false; |
| 498: | } |
| 499: | $data = array(); |
| 500: | while (false !== ($line = fgets($fp))) { |
| 501: | if (0 === strpos($line, $this->username . ':')) { |
| 502: | $data = explode(':', rtrim($line)); |
| 503: | break; |
| 504: | } |
| 505: | } |
| 506: | fclose($fp); |
| 507: | if (!$data) { |
| 508: | return false; |
| 509: | } |
| 510: | |
| 511: | if (!isset($data[1])) { |
| 512: | $str = 'Corrupted shadow: ' . $file . "\r\n" . |
| 513: | $this->username . "\r\n"; |
| 514: | Error_Reporter::report($str . "\r\n" . var_export($data, true)); |
| 515: | |
| 516: | return false; |
| 517: | } |
| 518: | $salt = implode('$', explode('$', $data[1])); |
| 519: | return Shadow::verify($password, $salt); |
| 520: | } |
| 521: | |
| 522: | |
| 523: | |
| 524: | |
| 525: | |
| 526: | |
| 527: | |
| 528: | public function verify_totp(#[SensitiveParameter] string $code): bool |
| 529: | { |
| 530: | return \Auth\TOTP::instantiateContexted($this->getAuthContext())->verify($code); |
| 531: | } |
| 532: | |
| 533: | |
| 534: | |
| 535: | |
| 536: | |
| 537: | |
| 538: | public function totp_exists(): bool |
| 539: | { |
| 540: | if (!IS_CLI) { |
| 541: | return $this->query('auth_totp_exists'); |
| 542: | } |
| 543: | |
| 544: | return \Auth\TOTP::instantiateContexted($this->getAuthContext())->exists(); |
| 545: | } |
| 546: | |
| 547: | |
| 548: | |
| 549: | |
| 550: | |
| 551: | |
| 552: | |
| 553: | |
| 554: | public function totp_enable(string $secret, string $code): bool |
| 555: | { |
| 556: | if (!IS_CLI) { |
| 557: | return $this->query('auth_totp_enable', $secret, $code); |
| 558: | } |
| 559: | |
| 560: | if ($this->is_demo()) { |
| 561: | return error("Feature disabled in demo"); |
| 562: | } |
| 563: | |
| 564: | $totp = \Auth\TOTP::instantiateContexted($this->getAuthContext()); |
| 565: | return $totp->verify($code, $secret) && |
| 566: | $totp->set($secret) && |
| 567: | \apnscpSession::invalidate_by_user($this->site_id, $this->username, $this->getAuthContext()->id) || error(\Auth\TOTP::ERR_TOTP_INVALID); |
| 568: | } |
| 569: | |
| 570: | |
| 571: | |
| 572: | |
| 573: | |
| 574: | |
| 575: | public function totp_disable(): bool |
| 576: | { |
| 577: | if (!IS_CLI) { |
| 578: | return $this->query('auth_totp_disable'); |
| 579: | } |
| 580: | |
| 581: | return \Auth\TOTP::instantiateContexted($this->getAuthContext())->disable(); |
| 582: | } |
| 583: | |
| 584: | |
| 585: | |
| 586: | |
| 587: | |
| 588: | |
| 589: | public function totp_code(): ?string |
| 590: | { |
| 591: | if (!IS_CLI) { |
| 592: | return $this->query('auth_totp_code'); |
| 593: | } |
| 594: | |
| 595: | return $this->totp_exists() ? null : \Auth\TOTP::instantiateContexted($this->getAuthContext())->generate(); |
| 596: | } |
| 597: | |
| 598: | |
| 599: | |
| 600: | |
| 601: | |
| 602: | |
| 603: | |
| 604: | public function set_extended_auth_flag(string $session = null): bool |
| 605: | { |
| 606: | if (posix_getuid()) { |
| 607: | return error("Setting EXTAUTH requires elevated privileges"); |
| 608: | } |
| 609: | |
| 610: | if (!\apnscpSession::init()->read($session ?? $this->session_id)) { |
| 611: | return error(\apnscpSession::ERR_INVALID_SESSION); |
| 612: | } |
| 613: | |
| 614: | $profile = \Auth_Info_User::factory($session ?? $this->session_id); |
| 615: | $profile->overrideLevel($profile->level | PRIVILEGE_EXTAUTH); |
| 616: | |
| 617: | return true; |
| 618: | } |
| 619: | |
| 620: | |
| 621: | |
| 622: | |
| 623: | |
| 624: | |
| 625: | |
| 626: | |
| 627: | |
| 628: | |
| 629: | |
| 630: | public function get_last_login(): array |
| 631: | { |
| 632: | $login = $this->get_login_history(1); |
| 633: | if (!$login) { |
| 634: | return array(); |
| 635: | } |
| 636: | |
| 637: | return $login[0]; |
| 638: | } |
| 639: | |
| 640: | |
| 641: | |
| 642: | |
| 643: | |
| 644: | |
| 645: | |
| 646: | |
| 647: | |
| 648: | |
| 649: | |
| 650: | |
| 651: | |
| 652: | |
| 653: | public function get_login_history(int $limit = null): array |
| 654: | { |
| 655: | $logins = array(); |
| 656: | |
| 657: | if ($this->is_demo()) { |
| 658: | $logins[] = array( |
| 659: | 'ip' => \Auth::client_ip(), |
| 660: | 'ts' => \Auth::login_time() |
| 661: | ); |
| 662: | |
| 663: | return $logins; |
| 664: | } |
| 665: | if (!is_null($limit) && $limit < 100) { |
| 666: | $limit = (int)$limit; |
| 667: | } else { |
| 668: | $limit = 10; |
| 669: | } |
| 670: | $limitStr = 'LIMIT ' . ($limit + 1); |
| 671: | $handler = \MySQL::initialize(); |
| 672: | $q = $handler->query("SELECT |
| 673: | UNIX_TIMESTAMP(`login_date`) AS login_date, |
| 674: | INET_NTOA(`ip`) AS ip FROM `login_log` |
| 675: | WHERE |
| 676: | `domain` = '" . $this->domain . "' |
| 677: | AND `username` = '" . $this->username . "' |
| 678: | ORDER BY id DESC " . $limitStr); |
| 679: | $q->fetch_object(); |
| 680: | |
| 681: | while (($data = $q->fetch_object()) !== null) { |
| 682: | $logins[] = array( |
| 683: | 'ip' => $data->ip, |
| 684: | 'ts' => $data->login_date |
| 685: | ); |
| 686: | } |
| 687: | |
| 688: | |
| 689: | |
| 690: | return $logins; |
| 691: | |
| 692: | } |
| 693: | |
| 694: | |
| 695: | |
| 696: | |
| 697: | |
| 698: | |
| 699: | |
| 700: | public function change_domain(string $domain): bool |
| 701: | { |
| 702: | if (!IS_CLI) { |
| 703: | $olddomain = $this->domain; |
| 704: | $ret = $this->query('auth_change_domain', $domain); |
| 705: | if ($ret) { |
| 706: | parent::sendNotice( |
| 707: | 'domain', |
| 708: | [ |
| 709: | 'email' => $this->getConfig('siteinfo', 'email'), |
| 710: | 'ip' => \Auth::client_ip(), |
| 711: | 'domain' => $olddomain |
| 712: | ] |
| 713: | ); |
| 714: | $this->_purgeLoginKey($this->username, $olddomain); |
| 715: | } |
| 716: | |
| 717: | return $ret; |
| 718: | } |
| 719: | |
| 720: | if ($this->is_demo()) { |
| 721: | return error('domain change disabled for demo'); |
| 722: | } |
| 723: | |
| 724: | $domain = strtolower($domain); |
| 725: | if (0 === strncmp($domain, "www.", 4)) { |
| 726: | $domain = substr($domain, 4); |
| 727: | } |
| 728: | if ($domain === $this->domain) { |
| 729: | return error('new domain is equivalent to old domain'); |
| 730: | } |
| 731: | if (!preg_match(Regex::DOMAIN, $domain)) { |
| 732: | return error("`%s': invalid domain", $domain); |
| 733: | } |
| 734: | if ($this->dns_domain_hosted($domain, true)) { |
| 735: | |
| 736: | return error("`%s': cannot add domain - hosted on another " . |
| 737: | 'account elsewhere', $domain); |
| 738: | } |
| 739: | |
| 740: | if ($this->web_subdomain_exists($domain)) { |
| 741: | return error("cannot promote subdomain `%s' to domain", $domain); |
| 742: | } |
| 743: | |
| 744: | if (\Opcenter\License::get()->isDevelopment() && substr($domain, -5) !== '.test') { |
| 745: | return error("License permits only .test TLDs. `%s' provided.", $domain); |
| 746: | } |
| 747: | |
| 748: | if (!$this->aliases_bypass_exists($domain) && |
| 749: | $this->dns_gethostbyname_t($domain) != $this->dns_get_public_ip() && |
| 750: | $this->dns_get_records_external('', 'any', $domain) && |
| 751: | !$this->dns_domain_uses_nameservers($domain) |
| 752: | ) { |
| 753: | $currentns = join(',', (array)$this->dns_get_authns_from_host($domain)); |
| 754: | $hostingns = join(',', $this->dns_get_hosting_nameservers($domain)); |
| 755: | |
| 756: | return error('domain uses third-party nameservers - %s, change nameservers to %s before promoting ' . |
| 757: | 'this domain to primary domain status', $currentns, $hostingns); |
| 758: | } |
| 759: | |
| 760: | $proc = new Util_Account_Editor($this->getAuthContext()->getAccount(), $this->getAuthContext()); |
| 761: | $proc->setConfig('siteinfo', 'domain', $domain)-> |
| 762: | setConfig(\Opcenter\SiteConfiguration::getModuleRemap('proftpd'), 'ftpserver', 'ftp' . $domain)-> |
| 763: | setConfig(\Opcenter\SiteConfiguration::getModuleRemap('apache'), 'webserver', 'www.' . $domain)-> |
| 764: | setConfig(\Opcenter\SiteConfiguration::getModuleRemap('sendmail'), 'mailserver', 'mail.' . $domain); |
| 765: | |
| 766: | return $proc->edit(); |
| 767: | } |
| 768: | |
| 769: | |
| 770: | |
| 771: | |
| 772: | |
| 773: | |
| 774: | |
| 775: | |
| 776: | private function _purgeLoginKey(string $user = '', string $domain = ''): void |
| 777: | { |
| 778: | |
| 779: | $userkey = md5($user . $domain); |
| 780: | $arrkey = self::SECURITY_TOKEN . '.' . $userkey; |
| 781: | $prefs = Preferences::factory($this->getAuthContext()); |
| 782: | $prefs->unlock($this->getApnscpFunctionInterceptor()); |
| 783: | unset($prefs[$arrkey]); |
| 784: | } |
| 785: | |
| 786: | |
| 787: | |
| 788: | |
| 789: | |
| 790: | |
| 791: | |
| 792: | public function change_username(string $user): bool |
| 793: | { |
| 794: | if (!IS_CLI) { |
| 795: | $email = $this->common_get_email(); |
| 796: | \Preferences::factory($this->getAuthContext())->sync(); |
| 797: | $ret = $this->query('auth_change_username', $user); |
| 798: | if ($ret && $email) { |
| 799: | |
| 800: | $this->getAuthContext()->username = $user; |
| 801: | |
| 802: | parent::sendNotice( |
| 803: | 'username', |
| 804: | [ |
| 805: | 'email' => $email, |
| 806: | 'ip' => \Auth::client_ip() |
| 807: | ] |
| 808: | ); |
| 809: | |
| 810: | $db = \apnscpSession::db(); |
| 811: | |
| 812: | $stmt = $db->prepare("UPDATE " . apnscpSession::TABLE . " SET |
| 813: | username = :user WHERE session_id = :session_id"); |
| 814: | $params = ['user' => $user, 'session_id' => $this->session_id]; |
| 815: | |
| 816: | if (!$stmt->execute($params)) { |
| 817: | \Error_Reporter::report($db->errorInfo()[0]); |
| 818: | fatal('failed to access credentials database'); |
| 819: | } |
| 820: | |
| 821: | $this->_purgeLoginKey($user, $this->domain); |
| 822: | } |
| 823: | |
| 824: | return $ret; |
| 825: | } |
| 826: | |
| 827: | if ($this->is_demo()) { |
| 828: | return error('username change disabled for demo'); |
| 829: | } |
| 830: | $user = strtolower($user); |
| 831: | if (!preg_match(Regex::USERNAME, $user)) { |
| 832: | return error("invalid new username `%s'", $user); |
| 833: | } |
| 834: | |
| 835: | |
| 836: | $class = \a23r::get_class_from_module('user'); |
| 837: | if (strlen($user) > $class::USER_MAXLEN) { |
| 838: | return error('user max length %d', $class::USER_MAXLEN); |
| 839: | } |
| 840: | |
| 841: | if ($this->permission_level & PRIVILEGE_ADMIN) { |
| 842: | |
| 843: | |
| 844: | if (!($fp = fopen(static::ADMIN_AUTH, 'r+')) || !flock($fp, LOCK_EX | LOCK_NB)) { |
| 845: | fclose($fp); |
| 846: | |
| 847: | return error("unable to gain exclusive lock on `%s'", static::ADMIN_AUTH); |
| 848: | } |
| 849: | $lines = []; |
| 850: | while (false !== ($line = fgets($fp))) { |
| 851: | $lines[] = explode(':', rtrim($line)); |
| 852: | } |
| 853: | if (false !== ($pos = array_search($user, array_column($lines, 0), true))) { |
| 854: | flock($fp, LOCK_UN); |
| 855: | fclose($fp); |
| 856: | |
| 857: | return error("user `%s' already exists", $user); |
| 858: | } |
| 859: | if (false === ($pos = array_search($this->username, array_column($lines, 0), true))) { |
| 860: | flock($fp, LOCK_UN); |
| 861: | fclose($fp); |
| 862: | |
| 863: | return error("original user `%s' does not exist", $this->username); |
| 864: | } |
| 865: | $lines[$pos][0] = $user; |
| 866: | if (!ftruncate($fp, 0)) { |
| 867: | flock($fp, LOCK_UN); |
| 868: | fclose($fp); |
| 869: | |
| 870: | return error("failed to truncate `%s'", static::ADMIN_AUTH); |
| 871: | } |
| 872: | rewind($fp); |
| 873: | fwrite($fp, implode("\n", array_map(static function ($a) { |
| 874: | return join(':', $a); |
| 875: | }, $lines))); |
| 876: | |
| 877: | |
| 878: | $oldprefs = implode(DIRECTORY_SEPARATOR, |
| 879: | [\Admin_Module::ADMIN_HOME, \Admin_Module::ADMIN_CONFIG, $this->username]); |
| 880: | $newprefs = implode(DIRECTORY_SEPARATOR, |
| 881: | [\Admin_Module::ADMIN_HOME, \Admin_Module::ADMIN_CONFIG, $user]); |
| 882: | if (file_exists($oldprefs)) { |
| 883: | if (file_exists($newprefs)) { |
| 884: | unlink($newprefs); |
| 885: | } |
| 886: | rename($oldprefs, $newprefs) || warn("failed to rename preferences from `%s' to `%s'", $oldprefs, $newprefs); |
| 887: | } |
| 888: | \apnscpSession::invalidate_by_user(null, $this->username); |
| 889: | return flock($fp, LOCK_UN) && fclose($fp); |
| 890: | } |
| 891: | |
| 892: | $this->user_flush(); |
| 893: | if (!$this->_username_unique($user)) { |
| 894: | return error("requested username `%s' in use on another account", $user); |
| 895: | } |
| 896: | |
| 897: | $this->getAuthContext()->username = $user; |
| 898: | $proc = new Util_Account_Editor($this->getAuthContext()->getAccount(), $this->getAuthContext()); |
| 899: | $proc->setConfig('siteinfo', 'admin_user', $user) |
| 900: | ->setConfig('mysql', 'dbaseadmin', $user); |
| 901: | $ret = $proc->edit(); |
| 902: | |
| 903: | if (!$ret) { |
| 904: | return error('failed to change admin user'); |
| 905: | } |
| 906: | |
| 907: | return true; |
| 908: | } |
| 909: | |
| 910: | |
| 911: | |
| 912: | |
| 913: | |
| 914: | |
| 915: | |
| 916: | |
| 917: | |
| 918: | private function _username_unique($user) |
| 919: | { |
| 920: | |
| 921: | $user = strtolower($user); |
| 922: | if (\Auth::get_admin_from_site_id($user)) { |
| 923: | return 0; |
| 924: | } |
| 925: | |
| 926: | if (!Auth_Lookup::extendedAvailable()) { |
| 927: | return 1; |
| 928: | } |
| 929: | |
| 930: | $db = self::_connect_db(); |
| 931: | if (!$db) { |
| 932: | return error('cannot connect to db'); |
| 933: | } |
| 934: | $q = "SELECT 1 FROM account_cache where admin = '" . |
| 935: | $db->real_escape_string($user) . "'"; |
| 936: | $rs = $db->query($q); |
| 937: | |
| 938: | return $rs->num_rows > 0 ? -1 : 1; |
| 939: | } |
| 940: | |
| 941: | private static function _connect_db() |
| 942: | { |
| 943: | if (!is_null(self::$domain_db) && self::$domain_db->ping()) { |
| 944: | return self::$domain_db; |
| 945: | } |
| 946: | try { |
| 947: | $db = new mysqli(AUTH_USERNAME_HOST, AUTH_USERNAME_USER, AUTH_USERNAME_PASSWORD, AUTH_USERNAME_DB); |
| 948: | } catch (\mysqli_sql_exception $e) { |
| 949: | return error('Cannot connect to domain server at this time'); |
| 950: | } |
| 951: | |
| 952: | self::$domain_db = &$db; |
| 953: | |
| 954: | return $db; |
| 955: | } |
| 956: | |
| 957: | |
| 958: | |
| 959: | |
| 960: | |
| 961: | |
| 962: | |
| 963: | |
| 964: | |
| 965: | public function set_temp_password(string $item, int $duration = 120, string $password = null) |
| 966: | { |
| 967: | if (!IS_CLI) { |
| 968: | return $this->query('auth_set_temp_password', $item, $duration, $password); |
| 969: | } |
| 970: | |
| 971: | if (!$password) { |
| 972: | $password = Password::generate(); |
| 973: | } |
| 974: | if ($duration < 1) { |
| 975: | return error("invalid duration `%d'", $duration); |
| 976: | } |
| 977: | |
| 978: | $user = null; |
| 979: | if ($this->permission_level & PRIVILEGE_ADMIN) { |
| 980: | if (0 !== strpos($item, 'site')) { |
| 981: | $tmp = \Auth::get_site_id_from_domain($item); |
| 982: | if (!$tmp) { |
| 983: | return error("domain `%s' not found on server", $item); |
| 984: | } |
| 985: | $item = 'site' . $tmp; |
| 986: | } else { |
| 987: | $tmp = \Auth::get_domain_from_site_id(substr($item, 4)); |
| 988: | if (!$tmp) { |
| 989: | return error("site `%s' not found on server", $item); |
| 990: | } |
| 991: | } |
| 992: | $site = $item; |
| 993: | $user = \Auth::get_admin_from_site_id(substr($site, 4)); |
| 994: | } else { |
| 995: | if (!\array_key_exists($item, $this->user_get_users())) { |
| 996: | return error("Unknown user `%s'", $item); |
| 997: | } |
| 998: | $site = $this->site; |
| 999: | $user = $item; |
| 1000: | } |
| 1001: | |
| 1002: | $ctx = \Auth::context($user, $site); |
| 1003: | if (!($oldcrypted = Shadow::bindTo($ctx->domain_fs_path())->getspnam($user))) { |
| 1004: | return error("Failed to locate shadow for `%s'", $user); |
| 1005: | } |
| 1006: | |
| 1007: | $crypted = $this->crypt($password); |
| 1008: | |
| 1009: | $accountMeta = $ctx->getAccount(); |
| 1010: | if ($this->permission_level & PRIVILEGE_ADMIN) { |
| 1011: | $editor = new Util_Account_Editor($accountMeta, $ctx); |
| 1012: | $ret = $editor->setMode('edit')->setConfig(self::getAuthService(), self::PWOVERRIDE_KEY, true) |
| 1013: | ->setConfig(self::getAuthService(), 'cpasswd', $crypted)->edit(); |
| 1014: | } else { |
| 1015: | $ret = Shadow::bindTo($ctx->domain_fs_path())->set_cpasswd($crypted, $user); |
| 1016: | } |
| 1017: | if (!$ret) { |
| 1018: | return error("failed to set temp password: `%s'", Error_Reporter::get_last_msg()); |
| 1019: | } |
| 1020: | |
| 1021: | |
| 1022: | $status = array( |
| 1023: | 'success' => true |
| 1024: | ); |
| 1025: | |
| 1026: | $dt = new DateTime("now + {$duration} seconds"); |
| 1027: | $proc = new Util_Process_Schedule($dt); |
| 1028: | $key = 'RESET-' . $ctx->user_id; |
| 1029: | if (!$proc->idPending($key, $this->getAuthContext())) { |
| 1030: | $proc->setID($key, $this->getAuthContext()); |
| 1031: | if ($this->permission_level & PRIVILEGE_ADMIN) { |
| 1032: | $editor = new Util_Account_Editor($accountMeta, $ctx); |
| 1033: | $editor->setMode('edit')->setConfig(self::getAuthService(), 'cpasswd', $oldcrypted['shadow'])-> |
| 1034: | setConfig(self::getAuthService(), self::PWOVERRIDE_KEY, false); |
| 1035: | $cmd = $editor->getCommand(); |
| 1036: | $args = null; |
| 1037: | } else { |
| 1038: | $chrtcmd = 'usermod -p ' . |
| 1039: | escapeshellarg($oldcrypted['shadow']) . ' ' . |
| 1040: | '"$(id -nu ' . $ctx->user_id . ')"'; |
| 1041: | $cmd = "chroot %(path)s /bin/sh -c '%(command)s'"; |
| 1042: | $args = [ |
| 1043: | 'command' => escapeshellarg($chrtcmd), |
| 1044: | 'path' => $ctx->domain_fs_path() |
| 1045: | ]; |
| 1046: | } |
| 1047: | $status = $proc->run($cmd, $args); |
| 1048: | } |
| 1049: | |
| 1050: | if ($status['success']) { |
| 1051: | info("Password set on `%s'@`%s' to `%s' for %d seconds", |
| 1052: | $ctx->username, |
| 1053: | $ctx->domain, |
| 1054: | $password, |
| 1055: | $duration |
| 1056: | ); |
| 1057: | } |
| 1058: | |
| 1059: | return $password; |
| 1060: | } |
| 1061: | |
| 1062: | |
| 1063: | |
| 1064: | |
| 1065: | |
| 1066: | |
| 1067: | |
| 1068: | |
| 1069: | |
| 1070: | |
| 1071: | private function _get_site_admin_shadow($site_id): string |
| 1072: | { |
| 1073: | $site = 'site' . (int)$site_id; |
| 1074: | $base = FILESYSTEM_VIRTBASE . "/{$site}/fst"; |
| 1075: | $file = '/etc/shadow'; |
| 1076: | $admin = \Auth::get_admin_from_site_id($site_id); |
| 1077: | if (!file_exists($base . $file)) { |
| 1078: | fatal("shadow not found for `%s'", $site); |
| 1079: | } |
| 1080: | $shadow = null; |
| 1081: | $fp = fopen($base . $file, 'r'); |
| 1082: | while (false !== ($line = fgets($fp))) { |
| 1083: | $tok = strtok($line, ':'); |
| 1084: | if ($tok != $admin) { |
| 1085: | continue; |
| 1086: | } |
| 1087: | $shadow = strtok(':'); |
| 1088: | break; |
| 1089: | } |
| 1090: | fclose($fp); |
| 1091: | if (!$shadow) { |
| 1092: | fatal("admin `%s' not found for `%s'", $admin, $site); |
| 1093: | } |
| 1094: | |
| 1095: | return $shadow; |
| 1096: | } |
| 1097: | |
| 1098: | public function _create() |
| 1099: | { |
| 1100: | static::rebuildMap(); |
| 1101: | } |
| 1102: | |
| 1103: | public function _edit() |
| 1104: | { |
| 1105: | $conf_new = $this->getAuthContext()->getAccount()->new; |
| 1106: | $conf_old = $this->getAuthContext()->getAccount()->old; |
| 1107: | $user = array( |
| 1108: | 'old' => $conf_old['siteinfo']['admin_user'], |
| 1109: | 'new' => $conf_new['siteinfo']['admin_user'] |
| 1110: | ); |
| 1111: | static::rebuildMap(); |
| 1112: | if ($user['old'] === $user['new']) { |
| 1113: | return; |
| 1114: | } |
| 1115: | |
| 1116: | return $this->_edit_wrapper($user['old'], $user['new']); |
| 1117: | } |
| 1118: | |
| 1119: | |
| 1120: | |
| 1121: | |
| 1122: | |
| 1123: | |
| 1124: | |
| 1125: | |
| 1126: | private function _edit_wrapper($userold, $usernew) |
| 1127: | { |
| 1128: | if ($userold === $usernew) { |
| 1129: | return; |
| 1130: | } |
| 1131: | $db = \MySQL::initialize(); |
| 1132: | foreach ($this->_get_api_keys_real($userold) as $key) { |
| 1133: | if (!$db->query("UPDATE api_keys SET `username` = '" . $db->escape_string($usernew) . "' " . |
| 1134: | "WHERE api_key = '" . $key['key'] . "' AND `username` = '" . $db->escape_string($userold) . "'" |
| 1135: | )) { |
| 1136: | warn("failed to rename API keys for user `%s' to `%s'", $userold, $usernew); |
| 1137: | } |
| 1138: | } |
| 1139: | |
| 1140: | $invoice = $this->billing_get_invoice(); |
| 1141: | if (!$db->query("UPDATE login_log SET `username` = '" . $db->escape_string($usernew) . "' " . |
| 1142: | "WHERE `username` = '" . $db->escape_string($userold) . "' AND invoice = '" . $db->escape_string($invoice) . "'")) { |
| 1143: | warn("failed to rename login history for user `%s' to `%s'", $userold, $usernew); |
| 1144: | } |
| 1145: | |
| 1146: | |
| 1147: | |
| 1148: | |
| 1149: | |
| 1150: | |
| 1151: | |
| 1152: | |
| 1153: | mute_warn(); |
| 1154: | foreach (static::PAM_SERVICES as $svc) { |
| 1155: | if ($this->user_permitted($userold, $svc)) { |
| 1156: | $this->deny_user($userold, $svc); |
| 1157: | $this->permit_user($usernew, $svc); |
| 1158: | } |
| 1159: | } |
| 1160: | unmute_warn(); |
| 1161: | |
| 1162: | $this->user_flush(); |
| 1163: | |
| 1164: | return true; |
| 1165: | } |
| 1166: | |
| 1167: | protected function _get_api_keys_real($user) |
| 1168: | { |
| 1169: | $db = Auth_SOAP::get_api_db(); |
| 1170: | $qfrag = $this->_getAPIQueryFragment(); |
| 1171: | |
| 1172: | |
| 1173: | |
| 1174: | |
| 1175: | $q = 'SELECT `api_key`, |
| 1176: | UNIX_TIMESTAMP(`last_used`) as last_used, |
| 1177: | comment |
| 1178: | FROM `api_keys` |
| 1179: | ' . $qfrag['join'] . " |
| 1180: | WHERE |
| 1181: | `username` = '" . $db->escape_string($user) . "' AND " . |
| 1182: | $qfrag['where'] . ' GROUP BY (api_key)'; |
| 1183: | $rs = $db->query($q); |
| 1184: | if (!$rs) { |
| 1185: | return error('failed to get keys'); |
| 1186: | } |
| 1187: | $keys = array(); |
| 1188: | while ($row = $rs->fetch_object()) { |
| 1189: | $keys[] = array( |
| 1190: | 'key' => $row->api_key, |
| 1191: | 'last_used' => $row->last_used, |
| 1192: | 'comment' => $row->comment |
| 1193: | ); |
| 1194: | } |
| 1195: | |
| 1196: | return $keys; |
| 1197: | } |
| 1198: | |
| 1199: | |
| 1200: | |
| 1201: | |
| 1202: | |
| 1203: | |
| 1204: | |
| 1205: | |
| 1206: | |
| 1207: | |
| 1208: | public function user_permitted(string $user = null, string $svc = 'cp'): bool |
| 1209: | { |
| 1210: | if (!in_array($svc, static::PAM_SERVICES, true)) { |
| 1211: | return error("unknown service `$svc'"); |
| 1212: | } |
| 1213: | |
| 1214: | if ($this->permission_level & (PRIVILEGE_ADMIN | PRIVILEGE_RESELLER)) { |
| 1215: | return $svc === 'cp'; |
| 1216: | } |
| 1217: | |
| 1218: | if ($this->permission_level & PRIVILEGE_USER) { |
| 1219: | $user = $this->username; |
| 1220: | } |
| 1221: | |
| 1222: | return (new Util_Pam($this->getAuthContext()))->check($user ?? $this->username, $svc); |
| 1223: | } |
| 1224: | |
| 1225: | |
| 1226: | |
| 1227: | |
| 1228: | |
| 1229: | |
| 1230: | |
| 1231: | |
| 1232: | public function user_enabled(string $user = null, string $svc = 'cp'): bool |
| 1233: | { |
| 1234: | deprecated_func('Use user_permitted'); |
| 1235: | return $this->user_permitted($user, $svc); |
| 1236: | } |
| 1237: | |
| 1238: | |
| 1239: | |
| 1240: | |
| 1241: | |
| 1242: | |
| 1243: | |
| 1244: | |
| 1245: | public function deny_user(string $user, string $svc = 'cp'): bool |
| 1246: | { |
| 1247: | return (new Util_Pam($this->getAuthContext()))->remove($user, $svc); |
| 1248: | } |
| 1249: | |
| 1250: | |
| 1251: | |
| 1252: | |
| 1253: | |
| 1254: | |
| 1255: | |
| 1256: | public function permit_user($user, $svc = 'cp'): bool |
| 1257: | { |
| 1258: | if (!in_array($svc, static::PAM_SERVICES, true)) { |
| 1259: | return error("unknown service `$svc'"); |
| 1260: | } |
| 1261: | |
| 1262: | return (new Util_Pam($this->getAuthContext()))->add($user, $svc); |
| 1263: | } |
| 1264: | |
| 1265: | |
| 1266: | |
| 1267: | |
| 1268: | |
| 1269: | |
| 1270: | |
| 1271: | |
| 1272: | public function restrict_ip(string $ip, string $gate = null): bool |
| 1273: | { |
| 1274: | if ($this->is_demo()) { |
| 1275: | return error('Cannot restrict IP in demo mode'); |
| 1276: | } |
| 1277: | return \Auth\IpRestrictor::instantiateContexted($this->getAuthContext())->add($ip, $gate); |
| 1278: | } |
| 1279: | |
| 1280: | |
| 1281: | |
| 1282: | |
| 1283: | |
| 1284: | |
| 1285: | |
| 1286: | |
| 1287: | public function remove_ip_restriction(string $ip, string $gate = null): bool |
| 1288: | { |
| 1289: | return \Auth\IpRestrictor::instantiateContexted($this->getAuthContext())->remove($ip, $gate); |
| 1290: | } |
| 1291: | |
| 1292: | |
| 1293: | |
| 1294: | |
| 1295: | |
| 1296: | |
| 1297: | public function get_ip_restrictions(): array |
| 1298: | { |
| 1299: | return \Auth\IpRestrictor::instantiateContexted($this->getAuthContext())->list(); |
| 1300: | } |
| 1301: | |
| 1302: | public function _edit_user(string $userold, string $usernew, array $oldpwd) |
| 1303: | { |
| 1304: | return $this->_edit_wrapper($userold, $usernew); |
| 1305: | } |
| 1306: | |
| 1307: | public function _reset(\Util_Account_Editor &$editor = null) |
| 1308: | { |
| 1309: | $module = self::getAuthService(); |
| 1310: | $crypted = $this->_get_site_admin_shadow($this->site_id); |
| 1311: | if (!$crypted) { |
| 1312: | fatal('call _reset() in auth from backend'); |
| 1313: | } |
| 1314: | $params = array( |
| 1315: | 'cpasswd' => $crypted |
| 1316: | ); |
| 1317: | if ($editor) { |
| 1318: | foreach ($params as $k => $v) { |
| 1319: | $editor->setConfig($module, $k, $v); |
| 1320: | } |
| 1321: | } |
| 1322: | |
| 1323: | return array($module => $params); |
| 1324: | |
| 1325: | } |
| 1326: | |
| 1327: | public function _delete() |
| 1328: | { |
| 1329: | |
| 1330: | |
| 1331: | |
| 1332: | $server = \Auth_Redirect::lookup($this->domain); |
| 1333: | if (!$server || $server === SERVER_NAME_SHORT) { |
| 1334: | foreach ($this->get_api_keys() as $key) { |
| 1335: | $this->delete_api_key($key['key']); |
| 1336: | } |
| 1337: | } |
| 1338: | } |
| 1339: | |
| 1340: | |
| 1341: | |
| 1342: | |
| 1343: | |
| 1344: | |
| 1345: | |
| 1346: | |
| 1347: | |
| 1348: | |
| 1349: | |
| 1350: | |
| 1351: | public function get_api_keys(string $user = null) |
| 1352: | { |
| 1353: | if (!$user || !($this->permission_level & PRIVILEGE_SITE)) { |
| 1354: | $user = $this->username; |
| 1355: | } else if ($user && !$this->user_exists($user)) { |
| 1356: | return error("user `%s' does not exist", $user); |
| 1357: | } |
| 1358: | |
| 1359: | return $this->_get_api_keys_real($user); |
| 1360: | } |
| 1361: | |
| 1362: | |
| 1363: | |
| 1364: | |
| 1365: | |
| 1366: | |
| 1367: | |
| 1368: | |
| 1369: | |
| 1370: | |
| 1371: | public function delete_api_key(string $key, string $user = null): bool |
| 1372: | { |
| 1373: | $key = str_replace('-', '', strtolower($key)); |
| 1374: | if (!ctype_xdigit($key)) { |
| 1375: | return error($key . ': invalid key'); |
| 1376: | } |
| 1377: | |
| 1378: | |
| 1379: | $keys = $this->get_api_keys($user); |
| 1380: | if (!$keys) { |
| 1381: | return false; |
| 1382: | } |
| 1383: | $found = false; |
| 1384: | foreach ($keys as $k) { |
| 1385: | if ($k['key'] === $key) { |
| 1386: | $found = true; |
| 1387: | break; |
| 1388: | } |
| 1389: | } |
| 1390: | if (!$found) { |
| 1391: | return error("unknown key `%s'", $key); |
| 1392: | } |
| 1393: | $db = Auth_SOAP::get_api_db(); |
| 1394: | $rs = $db->query("DELETE FROM `api_keys` |
| 1395: | WHERE `api_key` = '" . strtolower($key) . "'"); |
| 1396: | |
| 1397: | return (bool)$rs; |
| 1398: | } |
| 1399: | |
| 1400: | public function _housekeeping() |
| 1401: | { |
| 1402: | |
| 1403: | static::rebuildMap(); |
| 1404: | |
| 1405: | if (\Opcenter\License::get()->needsReissue()) { |
| 1406: | info('Attempting to renew apnscp license'); |
| 1407: | \Opcenter\License::get()->reissue(); |
| 1408: | } |
| 1409: | |
| 1410: | (new SecureAccessKey)->check(); |
| 1411: | } |
| 1412: | |
| 1413: | public function _cron(Cronus $cron) |
| 1414: | { |
| 1415: | $cron->schedule(FRONTEND_SECKEY_TTL, 'seckey.roll', static function () { |
| 1416: | (new SecureAccessKey)->reset(); |
| 1417: | }); |
| 1418: | |
| 1419: | $cron->schedule((int)FRONTEND_SECKEY_TTL/4, 'seckey.check', static function () { |
| 1420: | (new SecureAccessKey)->check(); |
| 1421: | }); |
| 1422: | } |
| 1423: | |
| 1424: | public function _create_user(string $user) |
| 1425: | { |
| 1426: | |
| 1427: | } |
| 1428: | |
| 1429: | public function _delete_user(string $user) |
| 1430: | { |
| 1431: | |
| 1432: | } |
| 1433: | |
| 1434: | public function _verify_conf(\Opcenter\Service\ConfigurationContext $ctx): bool |
| 1435: | { |
| 1436: | return true; |
| 1437: | } |
| 1438: | } |