PageRenderTime 24ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/inc/user.php

https://bitbucket.org/wez/mtrack/
PHP | 408 lines | 347 code | 50 blank | 11 comment | 56 complexity | 738e07891bf150423488e09c24283637 MD5 | raw file
Possible License(s): BSD-3-Clause, Apache-2.0
  1. <?php # vim:ts=2:sw=2:et:
  2. /* For copyright and licensing terms, see the file named LICENSE */
  3. /* An API for operating on users defined by the system */
  4. class MTrackUser {
  5. public $userid;
  6. public $fullname;
  7. public $email;
  8. public $timezone;
  9. public $active = true;
  10. public $sshkeys = null;
  11. public $aliases = null;
  12. public $prefs = null;
  13. private $stored = false;
  14. function __construct() {
  15. $this->prefs = new stdclass;
  16. }
  17. static function loadUser($username, $storedOnly = false) {
  18. $username = mtrack_canon_username($username);
  19. $data = MTrackDB::q('select userid, fullname, email, timezone, active, sshkeys, prefs from userinfo where userid = ?', $username)
  20. ->fetchAll(PDO::FETCH_ASSOC);
  21. if (!isset($data[0])) {
  22. if ($storedOnly) {
  23. return null;
  24. }
  25. $udata = MTrackAuth::getUserData($username);
  26. if (count($udata) == 1 && isset($udata['fullname'])) {
  27. /* we faked it; not a legitimate entry for our purposes */
  28. return null;
  29. }
  30. $data = array(
  31. 'userid' => $username,
  32. 'active' => true,
  33. 'fullname' => $udata['fullname'],
  34. 'email' => $udata['email'],
  35. 'timezone' => $udata['timezone'],
  36. );
  37. $stored = false;
  38. } else {
  39. $data = $data[0];
  40. $stored = true;
  41. }
  42. $user = new MTrackUser;
  43. $user->stored = $stored;
  44. $user->userid = $data['userid'];
  45. $user->fullname = $data['fullname'];
  46. $user->email = $data['email'];
  47. $user->timezone = $data['timezone'];
  48. $user->active = $data['active'];
  49. $user->sshkeys = $data['sshkeys'];
  50. $user->prefs = $data['prefs'];
  51. if ($user->prefs) {
  52. $user->prefs = json_decode($user->prefs);
  53. } else {
  54. $user->prefs = new stdclass;
  55. }
  56. $user->aliases = MTrackDB::q(<<<SQL
  57. select alias from useraliases where userid = ? order by alias
  58. SQL
  59. , $user->userid)->fetchAll(PDO::FETCH_COLUMN, 0);
  60. return $user;
  61. }
  62. function save(MTrackChangeset $CS) {
  63. if ($this->stored) {
  64. MTrackDB::q('update userinfo set fullname = ?, email = ?, timezone = ?, active = ?, sshkeys = ?, prefs = ? where userid = ?',
  65. $this->fullname,
  66. $this->email,
  67. $this->timezone,
  68. $this->active ? 1 : 0,
  69. $this->sshkeys,
  70. json_encode($this->prefs),
  71. $this->userid
  72. );
  73. } else {
  74. MTrackDB::q('insert into userinfo (active, fullname, email, timezone, sshkeys, userid, prefs) values (?, ?, ?, ?, ?, ?, ?)',
  75. $this->active ? 1 : 0,
  76. $this->fullname,
  77. $this->email,
  78. $this->timezone,
  79. $this->sshkeys,
  80. $this->userid,
  81. json_encode($this->prefs)
  82. );
  83. $this->stored = true;
  84. }
  85. if (MTrackACL::hasAllRights('User', 'modify')) {
  86. MTrackDB::q('delete from useraliases where userid = ?', $this->userid);
  87. foreach ($this->aliases as $alias) {
  88. if (!strlen(trim($alias))) {
  89. continue;
  90. }
  91. MTrackDB::q('insert into useraliases (userid, alias) values (?, ?)',
  92. $this->userid, $alias);
  93. }
  94. }
  95. }
  96. function getKeys() {
  97. $keys = array();
  98. $lines = preg_split("/\r?\n/", $this->sshkeys);
  99. foreach ($lines as $line) {
  100. if (!strlen($line)) continue;
  101. list($type, $key, $name) = preg_split("/\s+/", $line);
  102. $keys[$name] = array(
  103. 'id' => $name,
  104. 'key' => "$type $key"
  105. );
  106. }
  107. return $keys;
  108. }
  109. function addKey($name, $key) {
  110. if (!preg_match("/^ssh-\S+\s+(\S+)$/", $key)) {
  111. throw new Exception("invalid key");
  112. }
  113. $keys = $this->getKeys();
  114. $keys[$name] = array(
  115. 'id' => $name,
  116. 'key' => $key
  117. );
  118. $this->updateKeys($keys);
  119. }
  120. private function updateKeys($keys) {
  121. $new = array();
  122. foreach ($keys as $key) {
  123. $new[] = "$key[key] $key[id]";
  124. }
  125. $this->sshkeys = join("\n", $new);
  126. }
  127. function delKey($name) {
  128. $keys = $this->getKeys();
  129. unset($keys[$name]);
  130. $this->updateKeys($keys);
  131. }
  132. static function rest_perm_check($method, $user) {
  133. $me = mtrack_canon_username(MTrackAuth::whoami());
  134. $user = mtrack_canon_username($user);
  135. if ($user == $me) {
  136. /* I can read my data */
  137. return;
  138. }
  139. MTrackACL::requireAllRights('User', $method == 'GET' ? 'read' : 'modify');
  140. }
  141. function rest_return_user() {
  142. $u = MTrackAPI::makeObj($this, 'userid');
  143. $u->active = (bool)$u->active;
  144. unset($u->sshkeys);
  145. $u->role = MTrackAuth::getUserClass($u->id);
  146. $u->groups = array_values(MTrackAuth::getGroups($this->userid));
  147. /* ensure that we don't ever return the hash */
  148. unset($u->pwhash);
  149. return $u;
  150. }
  151. static function rest_user($method, $uri, $captures) {
  152. MTrackAPI::checkAllowed($method, 'GET', 'PUT');
  153. self::rest_perm_check($method, $captures['user']);
  154. $user = self::loadUser($captures['user']);
  155. if ($user === null && $method == 'GET') {
  156. MTrackAPI::error(404, "no such user", $captures);
  157. }
  158. if ($user === null) {
  159. $user = new MTrackUser();
  160. $user->userid = $captures['user'];
  161. }
  162. if ($method == 'PUT') {
  163. $in = MTrackAPI::getPayload();
  164. foreach (array('fullname', 'email', 'timezone', 'timezone',
  165. 'active', 'prefs') as $prop) {
  166. if (!isset($in->$prop)) continue;
  167. $user->$prop = $in->$prop;
  168. }
  169. if (MTrackACL::hasAllRights('User', 'modify')) {
  170. $user->aliases = $in->aliases;
  171. }
  172. $CS = MTrackChangeset::begin("user:$user->userid", "update");
  173. $user->save($CS);
  174. $CS->commit();
  175. if (MTrackACL::hasAllRights('User', 'modify')) {
  176. $user_class = MTrackAuth::getUserClass($user->userid);
  177. if (isset($in->role) && $in->role != $user_class) {
  178. MTrackConfig::set('user_classes', $user->userid, $in->role);
  179. MTrackConfig::save();
  180. }
  181. }
  182. }
  183. return $user->rest_return_user();
  184. }
  185. /** updates the stored password associated with the user.
  186. * We use SSHA512 for this */
  187. function setPassword($password) {
  188. /* make a salt */
  189. $salt = '';
  190. if (function_exists('openssl_random_psuedo_bytes')) {
  191. $salt = openssl_random_psuedo_bytes(4);
  192. } else {
  193. for ($i = 0; $i < 4; $i++) {
  194. $salt .= chr(mt_rand(0, 255));
  195. }
  196. }
  197. $digest = hash('sha512', $password . $salt, true);
  198. $pwhash = '{SSHA512}' . base64_encode($digest . $salt);
  199. MTrackDB::q('update userinfo set pwhash = ? where userid = ?',
  200. $pwhash, $this->userid);
  201. }
  202. /** verifies the provided password against the stored credential
  203. * information, returning true if the password matches */
  204. function verifyPassword($password)
  205. {
  206. foreach (MTrackDB::q('select pwhash from userinfo where userid = ?',
  207. $this->userid)->fetchAll(PDO::FETCH_COLUMN, 0) as $pwhash)
  208. {
  209. if (preg_match('/^\{([A-Z0-9]+)\}(.*)$/', $pwhash, $M)) {
  210. $mech = $M[1];
  211. $hash = $M[2];
  212. error_log("Loaded mech=$mech hash=$hash");
  213. switch ($mech) {
  214. case 'SSHA512':
  215. $d = base64_decode($hash);
  216. $salt = substr($d, 64);
  217. $hash = substr($d, 0, 64);
  218. return hash('sha512', $password . $salt, true) == $hash;
  219. }
  220. }
  221. }
  222. error_log("no entry for $this->userid ??");
  223. return false;
  224. }
  225. /** computes a new MD5 hash for the user based on the provided password */
  226. static function rest_password($method, $uri, $captures) {
  227. MTrackAPI::checkAllowed($method, 'POST');
  228. self::rest_perm_check($method, $captures['user']);
  229. $in = MTrackAPI::getPayload();
  230. $user = self::loadUser($captures['user']);
  231. if ($user === null) {
  232. MTrackACL::requireAllRights('User', 'create');
  233. $user = new MTrackUser();
  234. $user->userid = $captures['user'];
  235. $CS = MTrackChangeset::begin("user:$user->userid",
  236. "create and set password");
  237. $user->save($CS);
  238. $CS->commit();
  239. }
  240. $local_auth = MTrackAuth::getMech('MTrackAuth_MTrack');
  241. if ($local_auth) {
  242. $user->setPassword($in->password);
  243. return;
  244. }
  245. $http_auth = MTrackAuth::getMech('MTrackAuth_HTTP');
  246. if ($http_auth && !isset($_SERVER['REMOTE_USER'])) {
  247. $http_auth->setUserPassword($captures['user'], $in->password);
  248. return;
  249. }
  250. MTrackAPI::error(404, "HTTP authentication not configured");
  251. }
  252. static function rest_keys($method, $uri, $captures) {
  253. MTrackAPI::checkAllowed($method, 'GET');
  254. self::rest_perm_check($method, $captures['user']);
  255. $user = self::loadUser($captures['user']);
  256. if ($user === null) {
  257. MTrackAPI::error(404, "no such user", $captures);
  258. }
  259. $keys = array();
  260. foreach ($user->getKeys() as $k) {
  261. $keys[] = $k;
  262. }
  263. return $keys;
  264. }
  265. static function rest_velocity($method, $uri, $captures) {
  266. MTrackAPI::checkAllowed($method, 'GET');
  267. self::rest_perm_check($method, $captures['user']);
  268. return MTrackEBS::getVelocityDataForUser($captures['user']);
  269. }
  270. static function rest_predict($method, $uri, $captures) {
  271. MTrackAPI::checkAllowed($method, 'GET');
  272. self::rest_perm_check($method, $captures['user']);
  273. $mc = MTrackEBS::MonteCarlo($captures['user'], $captures['estimate']);
  274. $o = new stdclass;
  275. $o->montecarlo = array();
  276. $o->best_estimate = 0;
  277. $o->best_prob = 0;
  278. foreach ($mc as $est => $prob) {
  279. $o->best_prob = $prob;
  280. $o->best_estimate = $est;
  281. $o->montecarlo[] = array($est, $prob);
  282. }
  283. return $o;
  284. }
  285. static function rest_key($method, $uri, $captures) {
  286. MTrackAPI::checkAllowed($method, 'GET', 'PUT', 'POST', 'DELETE');
  287. self::rest_perm_check($method, $captures['user']);
  288. $user = self::loadUser($captures['user']);
  289. if ($user === null) {
  290. MTrackAPI::error(404, "no such user", $captures);
  291. }
  292. $name = $captures['key'];
  293. if ($method == 'GET') {
  294. $keys = $user->getKeys();
  295. if (isset($keys[$name])) {
  296. return $keys[$name];
  297. }
  298. MTrackAPI::error(404, "no such key", $captures);
  299. }
  300. if ($method == 'DELETE') {
  301. $keys = $user->getKeys();
  302. if (isset($keys[$name])) {
  303. $user->delKey($name);
  304. $CS = MTrackChangeset::begin(
  305. "user:$user->userid", "delete key $name");
  306. $user->save($CS);
  307. $CS->commit();
  308. return;
  309. }
  310. MTrackAPI::error(404, "no such key", $captures);
  311. }
  312. $key = MTrackAPI::getPayload();
  313. if (!is_object($key) || !isset($key->id) || !isset($key->key)) {
  314. MTrackAPI::error(400, "invalid key", $key);
  315. }
  316. $user->addKey($key->id, $key->key);
  317. $CS = MTrackChangeset::begin("user:$user->userid", "adding key $key->name");
  318. $user->save($CS);
  319. $CS->commit();
  320. }
  321. }
  322. class MTrackUserLink implements IMTrackLinkType {
  323. function resolveLinkToURL(MTrackLink $link)
  324. {
  325. $username = mtrack_canon_username($link->target);
  326. $link->class = 'userlink';
  327. $link->url = $GLOBALS['ABSWEB'] . 'user.php/' . $username;
  328. if ($link->label === null) {
  329. $link->label = $username;
  330. }
  331. }
  332. function renderHTMLLink(MTrackLink $link)
  333. {
  334. $username = mtrack_canon_username($link->target);
  335. if ($link->label === null) {
  336. $label = $username;
  337. } else {
  338. $label = $link->label;
  339. }
  340. if (!$link->label_is_html) {
  341. $label = htmlspecialchars($label, ENT_QUOTES, 'utf-8');
  342. }
  343. $class = '';
  344. if ($link->class) {
  345. $class = " class=\"$link->class\"";
  346. }
  347. $avatar = mtrack_avatar($username, 24);
  348. return "<a href=\"$link->url\"$class>$avatar $label</a>";
  349. }
  350. }
  351. MTrackAPI::register('/user/:user', 'MTrackUser::rest_user');
  352. MTrackAPI::register('/user/:user/keys', 'MTrackUser::rest_keys');
  353. MTrackAPI::register('/user/:user/password', 'MTrackUser::rest_password');
  354. MTrackAPI::register('/user/:user/keys/:key', 'MTrackUser::rest_key');
  355. MTrackAPI::register('/user/:user/ebs/velocity', 'MTrackUser::rest_velocity');
  356. MTrackAPI::register('/user/:user/ebs/predict/:estimate',
  357. 'MTrackUser::rest_predict');
  358. MTrackLink::register('user', '@MTrackUserLink');