liam-linux-account-manager/model/serveraccount.php

438 lines
18 KiB
PHP
Raw Permalink Normal View History

2021-11-16 15:11:32 +01:00
<?php
/**
* Class that represents an account on a server
*/
class ServerAccount extends Entity {
/**
* Defines the database table that this object is stored in
*/
protected $table = 'server_account';
/**
* Defines the field that is the primary key of the table
*/
protected $idfield = 'entity_id';
/**
* Magic getter method - if server field requested, return Server object
* @param string $field to retrieve
* @return mixed data stored in field
*/
public function &__get($field) {
global $user_dir;
switch($field) {
case 'server':
$server = new Server($this->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();
}
}
}