server_id); return $server; default: return parent::__get($field); } } /** * Write property changes to database and log the changes. * Triggers a resync of the server if account is activated/deactivated. */ public function update() { global $config; // Make it impossible to set default accounts to inactive if(is_array($config['defaults']['account_groups'])) { if(array_key_exists($this->data['name'], $config['defaults']['account_groups'])) { $this->data['active'] = true; } } $changes = parent::update(); $resync = false; foreach($changes as $change) { $loglevel = LOG_INFO; switch($change->field) { case 'active': if($this->sync_status != 'proposed') { $resync = true; } if($change->new_value == 1) $loglevel = LOG_WARNING; break; } $this->log(array('action' => 'Setting update', 'value' => $change->new_value, 'oldvalue' => $change->old_value, 'field' => ucfirst(str_replace('_', ' ', $change->field))), $loglevel); } if($resync) { $this->server->sync_access(); $this->sync_remote_access(); } } /** * List all log events for this server account. * @return array of ServerAccountEvent objects */ public function get_log() { if(is_null($this->id)) throw new BadMethodCallException('Server account must be in directory before log entries can be listed'); $stmt = $this->database->prepare(" SELECT * FROM entity_event WHERE entity_id = ? ORDER BY id DESC "); $stmt->bind_param('d', $this->id); $stmt->execute(); $result = $stmt->get_result(); $log = array(); while($row = $result->fetch_assoc()) { $log[] = new ServerAccountEvent($row['id'], $row); } $stmt->close(); return $log; } /** * Add the specified user as an administrator of the account. * This action is logged with a warning level as it is increasing an access level. * @param User $user to add as administrator */ public function add_admin(User $user) { global $config; parent::add_admin($user); $url = $config['web']['baseurl'].'/servers/'.urlencode($this->server->hostname).'/accounts/'.urlencode($this->name); $email = new Email; $email->subject = "Administrator for {$this->name}@{$this->server->hostname}"; $email->add_cc($config['email']['report_address'], $config['email']['report_name']); $email->add_recipient($user->email, $user->name); $email->body = "{$this->active_user->name} ({$this->active_user->uid}) has added you as an administrator for the '{$this->name}' account on {$this->server->hostname}. You can administer access to this account from <$url>"; $email->send(); $this->log(array('action' => 'Administrator add', 'value' => "user:{$user->uid}"), LOG_WARNING); } /** * Remove the specified user as an administrator of the account. * This action is logged with a warning level as it means the removed user will no longer * receive notifications for any changes done to this account. * @param User $user to remove as administrator */ public function delete_admin(User $user) { parent::delete_admin($user); $this->log(array('action' => 'Administrator remove', 'value' => "user:{$user->uid}"), LOG_WARNING); } /** * Add a public key to this account for use with any outbound access rules that apply to it. * An email is sent to the server admins and sec-ops to inform them of the change. * This action is logged with a warning level as it is potentially granting SSH access with the key. * @param PublicKey $key to be added */ public function add_public_key(PublicKey $key) { global $config; parent::add_public_key($key); if($this->active_user->uid != 'import-script') { $url = $config['web']['baseurl'].'/pubkeys/'.urlencode($key->id); $email = new Email; $email->add_reply_to($config['email']['admin_address'], $config['email']['admin_name']); foreach($this->server->list_effective_admins() as $admin) { $email->add_recipient($admin->email, $admin->name); } $email->add_cc($config['email']['report_address'], $config['email']['report_name']); $email->subject = "A new SSH public key has been added to the account {$this->name}@{$this->server->hostname} by {$this->active_user->uid}"; $email->body = "A new SSH public key has been added to the account {$this->name}@{$this->server->hostname} on SSH Key Authority. The key was added by {$this->active_user->name} ({$this->active_user->uid}).\n\nIf this key was added without your knowledge, please contact {$config['email']['admin_address']} immediately.\n\n".$key->summarize_key_information(); $email->send(); } $this->log(array('action' => 'Pubkey add', 'value' => $key->fingerprint_md5), LOG_WARNING); } /** * Delete the specified public key from this account. * @param PublicKey $key to be removed */ public function delete_public_key(PublicKey $key) { parent::delete_public_key($key); $this->log(array('action' => 'Pubkey remove', 'value' => $key->fingerprint_md5)); } /** * Request access for the specified entity (User/ServerAccount/Group) to this account. * Stores the request and sends an email to the account admins and server admins notifying them of it. * @param Entity $entity to request access for */ public function add_access_request(Entity $entity) { global $config; if(is_null($this->entity_id)) throw new BadMethodCallException('Server account must be added to server before access can be requested'); try { $request = new AccessRequest; $request->dest_entity_id = $this->entity_id; $request->source_entity_id = $entity->entity_id; $request->requested_by = $this->active_user->entity_id; $stmt = $this->database->prepare("INSERT INTO access_request SET dest_entity_id = ?, source_entity_id = ?, request_date = UTC_TIMESTAMP(), requested_by = ?"); $stmt->bind_param('ddd', $request->dest_entity_id, $request->source_entity_id, $request->requested_by); $stmt->execute(); $request->id = $stmt->insert_id; $stmt->close(); switch(get_class($entity)) { case 'User': $this->log(array('action' => 'Access request', 'value' => "user:{$entity->uid}")); break; case 'ServerAccount': $this->log(array('action' => 'Access request', 'value' => "account:{$entity->name}@{$entity->server->hostname}")); break; case 'Group': $this->log(array('action' => 'Access request', 'value' => "group:{$entity->name}")); break; } $account_admins = $this->list_admins(); $server_admins = $this->server->list_effective_admins(); if($this->active_user->uid != 'import-script') { $email = new Email; $email->add_reply_to($this->active_user->email, $this->active_user->name); if(count($account_admins) == 0) { foreach($server_admins as $admin) { $email->add_recipient($admin->email, $admin->name); } } else { foreach($account_admins as $admin) { $email->add_recipient($admin->email, $admin->name); } foreach($server_admins as $admin) { $email->add_cc($admin->email, $admin->name); } } $url = $config['web']['baseurl'].'/servers/'.urlencode($this->server->hostname).'/accounts/'.urlencode($this->name); switch(get_class($entity)) { case 'User': $email->subject = "{$entity->uid} requests access to {$this->name}@{$this->server->hostname}"; $email->body = "{$entity->name} ({$entity->uid}) has requested access to {$this->name}@{$this->server->hostname}. View this request at <$url>"; break; case 'ServerAccount': $email->subject = "{$this->active_user->uid} requests {$entity->name}@{$entity->server->hostname} access to {$this->name}@{$this->server->hostname}"; $email->body = "{$this->active_user->name} ({$this->active_user->uid}) has requested that {$entity->name}@{$entity->server->hostname} have server-to-server access to {$this->name}@{$this->server->hostname}. View this request at <$url>"; break; case 'Group': $email->subject = "{$this->active_user->uid} requests {$entity->name} group access to {$this->name}@{$this->server->hostname}"; $email->body = "{$this->active_user->name} ({$this->active_user->uid}) has requested that the {$entity->name} group have access to {$this->name}@{$this->server->hostname}. View this request at <$url>"; break; } $email->send(); } } catch(mysqli_sql_exception $e) { if($e->getCode() == 1062) { // Duplicate entry - ignore } else { throw $e; } } } /** * Approve a request for access to this account. * For user access, sends an email to the requester informing them of the approval. * Triggers add_access() and deletes the request from the DB. * @todo send emails for all access types * @param AccessRequest $request details */ public function approve_access_request(AccessRequest $request) { if(is_null($this->entity_id)) throw new BadMethodCallException('Server account must be added to server before access can be approved'); $entity = $request->source_entity; switch(get_class($entity)) { case 'User': $this->log(array('action' => 'Access approve', 'value' => "user:{$entity->uid}")); $email = new Email; $email->add_recipient($entity->email, $entity->name); $email->subject = "Your request for access to {$this->name}@{$this->server->hostname} has been approved"; $email->body = "You requested access to {$this->name}@{$this->server->hostname}, and this request has now been approved by {$this->active_user->name} ({$this->active_user->uid})."; $email->send(); break; case 'ServerAccount': $this->log(array('action' => 'Access approve', 'value' => "account:{$entity->name}@{$entity->server->hostname}")); break; case 'Group': $this->log(array('action' => 'Access approve', 'value' => "group:{$entity->name}")); break; } $options = array(); $this->add_access($entity, $options); $stmt = $this->database->prepare("DELETE FROM access_request WHERE dest_entity_id = ? AND id = ?"); $stmt->bind_param('dd', $this->entity_id, $request->id); $stmt->execute(); $stmt->close(); } /** * Reject a request for access to this account. * For user access, sends an email to the requester informing them of the rejection. * Deletes the request from the DB. If the account was created as the result of a request and * there are no other pending access requests for the account, deactivate the account. * @todo send emails for all access types * @param AccessRequest $request details */ public function reject_access_request(AccessRequest $request) { if(is_null($this->entity_id)) throw new BadMethodCallException('Server account must be added to server before access can be rejected'); $entity = $request->source_entity; switch(get_class($entity)) { case 'User': $this->log(array('action' => 'Access reject', 'value' => "user:{$entity->uid}")); $email = new Email; $email->add_recipient($entity->email, $entity->name); $email->subject = "Your request for access to {$this->name}@{$this->server->hostname} has been rejected"; $email->body = "You requested access to {$this->name}@{$this->server->hostname}, but this request has been rejected by {$this->active_user->name} ({$this->active_user->uid})."; $email->send(); break; case 'ServerAccount': $this->log(array('action' => 'Access reject', 'value' => "account:{$entity->name}@{$entity->server->hostname}")); break; case 'Group': $this->log(array('action' => 'Access reject', 'value' => "group:{$entity->name}")); break; } $stmt = $this->database->prepare("DELETE FROM access_request WHERE dest_entity_id = ? AND id = ?"); $stmt->bind_param('dd', $this->entity_id, $request->id); $stmt->execute(); $stmt->close(); if($this->sync_status == 'proposed') { if(count($this->list_access_requests()) == 0) { $this->active = 0; $this->update(); } } } /** * Grant the specified entity (User/ServerAccount/Group) access to this server account. * An email is sent to the account admins, server admins and sec-ops to inform them of the change. * This action is logged with a warning level as it is granting access. * @param Entity $entity to add as a group member * @param array $access_options array of AccessOption rules to apply to the granted access */ public function add_access(Entity $entity, array $access_options) { global $config; if(is_null($this->entity_id)) throw new BadMethodCallException('Server account must be added to server before access can be added'); if($this->sync_status == 'proposed') { $this->sync_status = 'not synced yet'; $this->update(); } try { $access = new Access; $access->dest_entity_id = $this->entity_id; $access->source_entity_id = $entity->entity_id; $access->granted_by = $this->active_user->entity_id; $stmt = $this->database->prepare("INSERT INTO access SET dest_entity_id = ?, source_entity_id = ?, grant_date = UTC_TIMESTAMP(), granted_by = ?"); $stmt->bind_param('ddd', $access->dest_entity_id, $access->source_entity_id, $access->granted_by); $stmt->execute(); $access->id = $stmt->insert_id; $stmt->close(); switch(get_class($entity)) { case 'User': $this->log(array('action' => 'Access add', 'value' => "user:{$entity->uid}"), LOG_WARNING); $mailsubject = "Access granted for {$entity->uid} to {$this->name}@{$this->server->hostname} by {$this->active_user->uid}"; $mailbody = "{$entity->name} ({$entity->uid}) has been granted access to {$this->name}@{$this->server->hostname} by {$this->active_user->name} ({$this->active_user->uid}). The changes will be synced to the server within a few seconds."; break; case 'ServerAccount': $this->log(array('action' => 'Access add', 'value' => "account:{$entity->name}@{$entity->server->hostname}"), LOG_WARNING); $mailsubject = "Access granted for {$entity->name}@{$entity->server->hostname} to {$this->name}@{$this->server->hostname} by {$this->active_user->uid}"; $mailbody = "{$entity->name}@{$entity->server->hostname} has been granted server-to-server access to {$this->name}@{$this->server->hostname} by {$this->active_user->name} ({$this->active_user->uid}). The changes will be synced to the server within a few seconds."; break; case 'Group': $this->log(array('action' => 'Access add', 'value' => "group:{$entity->name}"), LOG_WARNING); $mailsubject = "Access granted for {$entity->name} group to {$this->name}@{$this->server->hostname} by {$this->active_user->uid}"; $mailbody = "The {$entity->name} group has been granted access to {$this->name}@{$this->server->hostname} by {$this->active_user->name} ({$this->active_user->uid}). The changes will be synced to the server within a few seconds."; break; } if($this->active_user->uid != 'import-script') { $account_admins = $this->list_admins(); $server_admins = $this->server->list_effective_admins(); $email = new Email; if(count($account_admins) == 0) { foreach($server_admins as $admin) { $email->add_recipient($admin->email, $admin->name); } } else { foreach($account_admins as $admin) { $email->add_recipient($admin->email, $admin->name); } foreach($server_admins as $admin) { $email->add_cc($admin->email, $admin->name); } } $email->add_cc($config['email']['report_address'], $config['email']['report_name']); $email->subject = $mailsubject; $email->body = $mailbody; $email->send(); } foreach($access_options as $access_option) { $access->add_option($access_option); } } catch(mysqli_sql_exception $e) { if($e->getCode() == 1062) { // Duplicate entry - ignore } else { throw $e; } } $this->sync_access(); } /** * Revoke the specified access rule for this account. * @param Access $access rule to be removed */ public function delete_access(Access $access) { if(is_null($this->entity_id)) throw new BadMethodCallException('Server account must be added to server before access can be deleted'); $entity = $access->source_entity; switch(get_class($entity)) { case 'User': $this->log(array('action' => 'Access remove', 'value' => "user:{$entity->uid}")); break; case 'ServerAccount': $this->log(array('action' => 'Access remove', 'value' => "account:{$entity->name}@{$entity->server->hostname}")); break; case 'Group': $this->log(array('action' => 'Access remove', 'value' => "group:{$entity->name}")); break; } $stmt = $this->database->prepare("DELETE FROM access WHERE dest_entity_id = ? AND id = ?"); $stmt->bind_param('dd', $this->entity_id, $access->id); $stmt->execute(); $stmt->close(); $this->sync_access(); } /** * List all groups that this account is a member of. * @return array of Group objects */ public function list_group_membership() { global $group_dir; return $group_dir->list_group_membership($this); } /** * Trigger a sync for this account. */ public function sync_access() { global $sync_request_dir; $sync_request = new SyncRequest; $sync_request->server_id = $this->server_id; $sync_request->account_name = $this->name; $sync_request_dir->add_sync_request($sync_request); } /** * Determine if a sync is currently pending for this account. * @return boolean true if a sync is pending */ public function sync_is_pending() { $stmt = $this->database->prepare("SELECT * FROM sync_request WHERE server_id = ? AND (account_name = ? OR account_name IS NULL) ORDER BY account_name"); $stmt->bind_param('ds', $this->server_id, $this->name); $stmt->execute(); $result = $stmt->get_result(); return $result->num_rows > 0; } /** * Update the sync status for the account. * @param string $status "sync success", "sync failure" or "sync warning" */ public function sync_report($status) { if(is_null($this->id)) throw new BadMethodCallException('Server account must be in directory before sync reporting can be done'); if($this->sync_status != 'proposed') { $this->sync_status = $status; $this->update(); } } }