You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

334 lines
14 KiB
PHP

<?php
/**
* Class that represents a grouping of users or server accounts
*/
class Group extends Entity {
/**
* Defines the database table that this object is stored in
*/
protected $table = 'group';
/**
* Defines the field that is the primary key of the table
*/
protected $idfield = 'entity_id';
public function __construct($id = null, $preload_data = array()) {
parent::__construct($id, $preload_data);
if(!isset($this->data['system'])) $this->data['system'] = 0;
}
/**
* Write property changes to database and log the changes.
* Triggers a resync if the group was activated/deactivated.
*/
public function update() {
if($this->data['system']) $this->data['active'] = 1; // Cannot disable system groups
$changes = parent::update();
$resync = false;
foreach($changes as $change) {
$loglevel = LOG_INFO;
switch($change->field) {
case 'active':
$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->sync_access();
$this->sync_remote_access();
}
}
/**
* List all log events for this group.
* @return array of GroupEvent objects
*/
public function get_log() {
if(is_null($this->id)) throw new BadMethodCallException('Group 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 GroupEvent($row['id'], $row);
}
$stmt->close();
return $log;
}
/**
* Add the specified user as an administrator of the group.
* 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'].'/groups/'.urlencode($this->name);
$email = new Email;
$email->subject = "Administrator for {$this->name} group";
$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}' group. You can administer this group 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 group.
* 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 group.
* @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 the specified entity (User/ServerAccount/Group†) as a member of the group.
* †Adding a Group as a member of a group (nested groups) is no longer allowed by the UI.
* This action is logged with a warning level as it is potentially granting access.
* @todo remove nested group functionality
* @param Entity $entity to add as a group member
*/
public function add_member(Entity $entity) {
global $config;
if(is_null($this->entity_id)) throw new BadMethodCallException('Group must be in directory before members can be added');
if(is_null($entity->entity_id)) throw new InvalidArgumentException('Entity must be in directory before it can be added to a group');
$entity_id = $entity->entity_id;
switch(get_class($entity)) {
case 'User':
$name = "user {$entity->uid}";
$mailsubject = "{$entity->uid} added to {$this->name} group by {$this->active_user->uid}";
$mailbody = "{$entity->name} ({$entity->uid}) has been added to the {$this->name} group by {$this->active_user->name} ({$this->active_user->uid}).";
$logmsg = array('action' => 'Member add', 'value' => "user:{$entity->uid}");
break;
case 'ServerAccount':
// We should not allow adding server accounts to a group if the active user is not an admin of that server or server account
if(!$this->active_user->admin && !$this->active_user->admin_of($entity->server) && !$this->active_user->admin_of($entity)) {
throw new InvalidArgumentException('Active user is not an administrator of the specified server account');
}
$name = "account {$entity->name}@{$entity->server->hostname}";
$mailsubject = "{$entity->name}@{$entity->server->hostname} added to {$this->name} group by {$this->active_user->uid}";
$mailbody = "{$entity->name}@{$entity->server->hostname} has been added to the {$this->name} group by {$this->active_user->name} ({$this->active_user->uid}).";
$logmsg = array('action' => 'Member add', 'value' => "account:{$entity->name}@{$entity->server->hostname}");
break;
case 'Group':
// We should not allow adding groups to a group if the active user is not an admin of that group
if(!$this->active_user->admin && !$this->active_user->admin_of($entity)) {
throw new InvalidArgumentException('Active user is not an administrator of the specified group');
}
$name = "group {$entity->name}";
$mailsubject = "{$entity->name} group added to {$this->name} group by {$this->active_user->uid}";
$mailbody = "The {$entity->name} group has been added to the {$this->name} group by {$this->active_user->name} ({$this->active_user->uid}).";
$logmsg = array('action' => 'Member add', 'value' => "group:{$entity->name}");
break;
}
try {
$stmt = $this->database->prepare("INSERT INTO group_member SET `group` = ?, entity_id = ?, add_date = UTC_TIMESTAMP(), added_by = ?");
$stmt->bind_param('ddd', $this->entity_id, $entity_id, $this->active_user->entity_id);
$stmt->execute();
$stmt->close();
$this->log($logmsg, LOG_WARNING);
if($this->active_user->uid != 'import-script') {
$email = new Email;
foreach($this->list_admins() as $admin) {
$email->add_recipient($admin->email, $admin->name);
}
$email->add_cc($config['email']['report_address'], $config['email']['report_name']);
$email->subject = $mailsubject;
$email->body = $mailbody;
$email->send();
}
} catch(mysqli_sql_exception $e) {
if($e->getCode() == 1062) {
// Duplicate entry - ignore
} else {
throw $e;
}
}
$entity->sync_access(); // This entity is now a member of the group, so any access rules that apply to the group now apply to the entity
$this->sync_remote_access(); // If this group has access to anything, this entity now also has access to it
}
/**
* Remove the specified entity (User/ServerAccount/Group) as a member of the group.
* @todo remove nested group functionality
* @param Entity $entity to remove as a group member
*/
public function delete_member(Entity $entity) {
if(is_null($this->entity_id)) throw new BadMethodCallException('Group must be in directory before members can be deleted');
switch(get_class($entity)) {
case 'User':
$this->log(array('action' => 'Member remove', 'value' => "user:{$entity->uid}"));
break;
case 'ServerAccount':
$this->log(array('action' => 'Member remove', 'value' => "account:{$entity->name}@{$entity->server->hostname}"));
break;
case 'Group':
$this->log(array('action' => 'Member remove', 'value' => "group:{$entity->name}"));
break;
}
$stmt = $this->database->prepare("DELETE FROM group_member WHERE `group` = ? AND entity_id = ?");
$stmt->bind_param('ds', $this->entity_id, $entity->entity_id);
$stmt->execute();
$stmt->close();
// Resync both the entity being removed and the group itself
$entity->sync_access();
$this->sync_remote_access();
}
/**
* List all members of the group.
* @todo remove nested group functionality
* @return array of User/ServerAccount/Group objects
*/
public function list_members() {
if(is_null($this->entity_id)) throw new BadMethodCallException('Group must be in directory before members can be listed');
$stmt = $this->database->prepare("
SELECT entity.id, entity.type, add_date, added_by
FROM group_member
INNER JOIN entity ON group_member.entity_id = entity.id
LEFT JOIN user ON user.entity_id = entity.id
LEFT JOIN server_account ON server_account.entity_id = entity.id
LEFT JOIN server ON server.id = server_account.server_id
LEFT JOIN `group` ON `group`.entity_id = entity.id
WHERE group_member.group = ?
ORDER BY entity.type, user.uid, server.hostname, server_account.name, `group`.name
");
$stmt->bind_param('d', $this->entity_id);
$stmt->execute();
$result = $stmt->get_result();
$members = array();
while($row = $result->fetch_assoc()) {
$row['added_by'] = new User($row['added_by']);
switch($row['type']) {
case 'user': $members[] = new User($row['id'], $row); break;
case 'server account': $members[] = new ServerAccount($row['id'], $row); break;
case 'group': $members[] = new Group($row['id'], $row); break;
}
}
$stmt->close();
return $members;
}
/**
* Grant the specified entity (User/ServerAccount/Group) access to members of this group.
* An email is sent to the group 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('Group must be in directory before access can be added');
if(is_null($entity->entity_id)) throw new InvalidArgumentException('Entity must be in directory before it can be granted access to a group');
$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;
try {
$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 = "{$entity->uid} granted access to {$this->name} group resources by {$this->active_user->uid}";
$mailbody = "{$entity->name} ({$entity->uid}) has been granted access to resources in the {$this->name} group by {$this->active_user->name} ({$this->active_user->uid}).";
break;
case 'ServerAccount':
$this->log(array('action' => 'Access add', 'value' => "account:{$entity->name}@{$entity->server->hostname}"), LOG_WARNING);
$mailsubject = "{$entity->name}@{$entity->server->hostname} granted access to {$this->name} group resources by {$this->active_user->uid}";
$mailbody = "{$entity->name}@{$entity->server->hostname} has been granted access to resources in the {$this->name} group by {$this->active_user->name} ({$this->active_user->uid}).";
break;
case 'Group':
$this->log(array('action' => 'Access add', 'value' => "group:{$entity->name}"), LOG_WARNING);
$mailsubject = "{$entity->name} group granted access to {$this->name} group resources by {$this->active_user->uid}";
$mailbody = "The {$entity->name} group has been granted access to resources in the {$this->name} group by {$this->active_user->name} ({$this->active_user->uid}).";
break;
}
if($this->active_user->uid != 'import-script') {
$email = new Email;
foreach($this->list_admins() as $admin) {
$email->add_recipient($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 to members of this group.
* @param Access $access rule to be removed
*/
public function delete_access(Access $access) {
if(is_null($this->entity_id)) throw new BadMethodCallException('Group must be in directory 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('ds', $this->entity_id, $access->id);
$stmt->execute();
$stmt->close();
$this->sync_access();
}
/**
* List all groups that *this* group is a member of, searched recursively.
* Note: nested groups are no longer allowed by the UI.
* @todo remove nested group functionality
* @return array of Group objects
*/
public function list_group_membership() {
global $group_dir;
return $group_dir->list_group_membership($this);
}
/**
* Trigger a resync for all members of this group, searched recursively†.
* †Nested groups are no longer allowed by the UI.
* @todo remove nested group functionality
* @param array $seen keep track of entities we've already processed to prevent infinite recursion
*/
public function sync_access(&$seen = array()) {
$seen[$this->entity_id] = true;
$members = $this->list_members();
foreach($members as $entity) {
if(!isset($seen[$entity->entity_id])) {
$entity->sync_access($seen);
}
}
}
}