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