liam-linux-account-manager/model/entity.php
2021-11-16 15:11:32 +01:00

402 lines
15 KiB
PHP

<?php
/**
* Abstract class that represents one of several types of entities (users, server accounts, groups)
* which can have access rules created between them, administrators assigned, or be members of each other.
*/
abstract class Entity extends Record {
/**
* Defines the database table that this object is stored in
*/
protected $table = 'entity';
/**
* Write event details to syslog and to entity_event table.
* @param array $details event paramaters to be logged
* @param int $level syslog priority as defined in http://php.net/manual/en/function.syslog.php
*/
public function log($details, $level = LOG_INFO) {
if(is_null($this->id)) throw new BadMethodCallException('Entity must be in directory before log entries can be added');
switch(get_class($this)) {
case 'User':
$scope = "user:{$this->uid}";
break;
case 'ServerAccount':
$scope = "account:{$this->name}@{$this->server->hostname}";
break;
case 'Group':
$scope = "group:{$this->name}";
break;
default:
throw new BadMethodCallException('Unsupported entity type: '.get_class($this));
}
$json = json_encode($details, JSON_UNESCAPED_UNICODE);
$stmt = $this->database->prepare("INSERT INTO entity_event SET entity_id = ?, actor_id = ?, date = UTC_TIMESTAMP(), details = ?");
$stmt->bind_param('dds', $this->id, $this->active_user->entity_id, $json);
$stmt->execute();
$stmt->close();
$text = "KeysScope=\"{$scope}\" KeysRequester=\"{$this->active_user->uid}\"";
foreach($details as $key => $value) {
$text .= ' Keys'.ucfirst($key).'="'.str_replace('"', '', $value).'"';
}
openlog('keys', LOG_ODELAY, LOG_AUTH);
syslog($level, $text);
closelog();
}
/**
* Add the specified user as an administrator of the entity.
* Logging is performed by the inheriting classes.
* @param User $user to add as administrator
*/
public function add_admin(User $user) {
if(is_null($this->entity_id)) throw new BadMethodCallException('Entity must be in directory before admins can be added');
if(is_null($user->entity_id)) throw new InvalidArgumentException('User must be in directory before it can be made admin');
$entity_id = $user->entity_id;
try {
$stmt = $this->database->prepare("INSERT INTO entity_admin SET entity_id = ?, admin = ?");
$stmt->bind_param('dd', $this->entity_id, $entity_id);
$stmt->execute();
$stmt->close();
} catch(mysqli_sql_exception $e) {
if($e->getCode() == 1062) {
// Duplicate entry - ignore
} else {
throw $e;
}
}
}
/**
* Remove the specified user as an administrator of the entity.
* @param User $user to remove as administrator
*/
public function delete_admin(User $user) {
if(is_null($this->entity_id)) throw new BadMethodCallException('Entity must be in directory before admins can be deleted');
if(is_null($user->entity_id)) throw new InvalidArgumentException('User must be in directory before it can be removed as admin');
$entity_id = $user->entity_id;
$stmt = $this->database->prepare("DELETE FROM entity_admin WHERE entity_id = ? AND admin = ?");
$stmt->bind_param('dd', $this->entity_id, $entity_id);
$stmt->execute();
$stmt->close();
}
/**
* List all administrators of this entity.
* @return array of User objects
*/
public function list_admins() {
if(is_null($this->entity_id)) throw new BadMethodCallException('Entity must be in directory before admins can be listed');
$stmt = $this->database->prepare("SELECT admin FROM entity_admin WHERE entity_id = ?");
$stmt->bind_param('d', $this->entity_id);
$stmt->execute();
$result = $stmt->get_result();
$admins = array();
while($row = $result->fetch_assoc()) {
$admins[] = new User($row['admin']);
}
$stmt->close();
return $admins;
}
/**
* Add a public key to this entity for use with any outbound access rules that apply to it.
* Emailing and logging is handled by the inheriting classes.
* @param PublicKey $key to be added
*/
public function add_public_key(PublicKey $key) {
if(is_null($this->entity_id)) throw new BadMethodCallException('Entity must be in directory before public keys can be added');
$key->get_openssh_info();
$key_type = $key->type;
$key_keydata = $key->keydata;
$key_comment = $key->comment;
$key_size = $key->keysize;
$key_fingerprint_md5 = $key->fingerprint_md5;
$key_fingerprint_sha256 = $key->fingerprint_sha256;
$key_randomart_md5 = $key->randomart_md5;
$key_randomart_sha256 = $key->randomart_sha256;
$stmt = $this->database->prepare("
INSERT INTO public_key SET
entity_id = ?,
type = ?,
keydata = ?,
comment = ?,
keysize = ?,
fingerprint_md5 = ?,
fingerprint_sha256 = ?,
randomart_md5 = ?,
randomart_sha256 = ?
");
$stmt->bind_param('dsssdssss', $this->entity_id, $key_type, $key_keydata, $key_comment, $key_size, $key_fingerprint_md5, $key_fingerprint_sha256, $key_randomart_md5, $key_randomart_sha256);
$stmt->execute();
$key->id = $stmt->insert_id;
$stmt->close();
$this->sync_remote_access();
}
/**
* Delete the specified public key from this entity.
* @param PublicKey $key to be removed
*/
public function delete_public_key(PublicKey $key) {
if(is_null($this->entity_id)) throw new BadMethodCallException('Entity must be in directory before public keys can be deleted');
$stmt = $this->database->prepare("DELETE FROM public_key WHERE entity_id = ? AND id = ?");
$stmt->bind_param('dd', $this->entity_id, $key->id);
$stmt->execute();
$stmt->close();
$this->sync_remote_access();
}
/**
* Retrieve a specific public key for this entity by its ID.
* @param int $id of public key to retrieve
* @return PublicKey matching the ID
* @throws PublicKeyNotFoundException if no public key exists with that ID
*/
public function get_public_key_by_id($id) {
if(is_null($this->entity_id)) throw new BadMethodCallException('Entity must be in directory before public keys can be listed');
$stmt = $this->database->prepare("SELECT * FROM public_key WHERE entity_id = ? AND id = ?");
$stmt->bind_param('dd', $this->entity_id, $id);
$stmt->execute();
$result = $stmt->get_result();
if($row = $result->fetch_assoc()) {
$key = new PublicKey($row['id'], $row);
} else {
throw new PublicKeyNotFoundException('Public key does not exist.');
}
$stmt->close();
return $key;
}
/**
* List all public keys associated with this entity, optionally filtered by account name and hostname
* for any of the keys that have destination rules applied.
* @todo this is perhaps an unintuitive place to do this kind of filtering
* @param string|null $account_name to filter for in the destination rules for each key
* @param string|null $hostname to filter for in the destination rules for each key
* @return array of PublicKey objects
*/
public function list_public_keys($account_name = null, $hostname = null) {
if(is_null($this->entity_id)) throw new BadMethodCallException('Entity must be in directory before public keys can be listed');
$stmt = $this->database->prepare("
SELECT public_key.*, COUNT(public_key_dest_rule.id) AS dest_rule_count
FROM public_key
LEFT JOIN public_key_dest_rule ON public_key_dest_rule.public_key_id = public_key.id
WHERE entity_id = ?
GROUP BY public_key.id
");
$stmt->bind_param('d', $this->entity_id);
$stmt->execute();
$result = $stmt->get_result();
$keys = array();
while($row = $result->fetch_assoc()) {
if((is_null($account_name) && is_null($hostname)) || $row['dest_rule_count'] == 0) {
$include = true;
} else {
$include = false;
$rulestmt = $this->database->prepare("SELECT * FROM public_key_dest_rule WHERE public_key_id = ?");
$rulestmt->bind_param('d', $row['id']);
$rulestmt->execute();
$ruleresult = $rulestmt->get_result();
if($ruleresult->num_rows == 0) {
// Key has no destination rules defined, include it everywhere
$include = true;
} else {
// Apply destination rules
while($rule = $ruleresult->fetch_assoc()) {
$filter1 = '/^'.str_replace('\*', '.*', preg_quote($rule['account_name_filter'], '/')).'$/i';
$filter2 = '/^'.str_replace('\*', '.*', preg_quote($rule['hostname_filter'], '/')).'$/i';
if(preg_match($filter1, $account_name) && preg_match($filter2, $hostname)) {
$include = true;
break;
}
}
}
}
if($include) {
$keys[] = new PublicKey($row['id'], $row);
}
}
$stmt->close();
return $keys;
}
/**
* Retrieve a specific access rule towards this entity by its ID (inbound access).
* @param int $id to retrieve
* @return Access object
* @throws AccessNotFoundException if no access rule exists with this ID
*/
public function get_access_by_id($id) {
if(is_null($this->entity_id)) throw new BadMethodCallException('Entity must be in directory before access can be listed');
$stmt = $this->database->prepare("
SELECT access.*, entity.type
FROM access
INNER JOIN entity ON entity.id = access.source_entity_id
WHERE access.dest_entity_id = ? AND access.id = ?
");
$stmt->bind_param('dd', $this->entity_id, $id);
$stmt->execute();
$result = $stmt->get_result();
if($row = $result->fetch_assoc()) {
switch($row['type']) {
case 'user': $source_entity = new User($row['source_entity_id']); break;
case 'server account': $source_entity = new ServerAccount($row['source_entity_id']); break;
case 'group': $source_entity = new Group($row['source_entity_id']); break;
}
$row['granted_by'] = new User($row['granted_by']);
$row['source_entity'] = $source_entity;
$row['dest_entity'] = $this;
$access = new Access($row['id'], $row);
} else {
throw new AccessNotFoundException('Access rule does not exist.');
}
$stmt->close();
return $access;
}
/**
* List all access rules that grant access to this entity (inbound access).
* @return array of Access objects
*/
public function list_access() {
if(is_null($this->entity_id)) throw new BadMethodCallException('Entity must be in directory before access can be listed');
$stmt = $this->database->prepare("
SELECT access.*, entity.type
FROM access
INNER JOIN entity ON entity.id = access.source_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 dest_entity_id = ?
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();
$access_list = array();
while($row = $result->fetch_assoc()) {
switch($row['type']) {
case 'user': $source_entity = new User($row['source_entity_id']); break;
case 'server account': $source_entity = new ServerAccount($row['source_entity_id']); break;
case 'group': $source_entity = new Group($row['source_entity_id']); break;
}
$row['granted_by'] = new User($row['granted_by']);
$row['source_entity'] = $source_entity;
$access_list[] = new Access($row['id'], $row);
}
$stmt->close();
return $access_list;
}
/**
* List all requests for access to this entity (inbound access).
* @return array of AccessRequest objects
*/
public function list_access_requests() {
if(is_null($this->entity_id)) throw new BadMethodCallException('Entity must be in directory before access can be listed');
$stmt = $this->database->prepare("
SELECT access_request.*, entity.type
FROM access_request
INNER JOIN entity ON entity.id = access_request.source_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 dest_entity_id = ?
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();
$access_requests = array();
while($row = $result->fetch_assoc()) {
switch($row['type']) {
case 'user': $source_entity = new User($row['source_entity_id']); break;
case 'server account': $source_entity = new ServerAccount($row['source_entity_id']); break;
case 'group': $source_entity = new Group($row['source_entity_id']); break;
}
$row['requested_by'] = new User($row['requested_by']);
$row['source_entity'] = $source_entity;
$access_requests[] = new AccessRequest($row['id'], $row);
}
$stmt->close();
return $access_requests;
}
/**
* List all access rules that grant this entity access to other entities (outbound access).
* @return array of Access objects
*/
public function list_remote_access() {
if(is_null($this->entity_id)) throw new BadMethodCallException('Entity must be in directory before remote access can be listed');
$stmt = $this->database->prepare("
SELECT access.*, entity.type
FROM access
INNER JOIN entity ON access.dest_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 access.source_entity_id = ?
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();
$access_list = array();
while($row = $result->fetch_assoc()) {
switch($row['type']) {
case 'user': $dest_entity = new User($row['dest_entity_id']); break;
case 'server account': $dest_entity = new ServerAccount($row['dest_entity_id']); break;
case 'group': $dest_entity = new Group($row['dest_entity_id']); break;
}
$row['granted_by'] = new User($row['granted_by']);
$row['dest_entity'] = $dest_entity;
$access_list[] = new Access($row['id'], $row);
}
$stmt->close();
return $access_list;
}
/**
* Trigger a sync for this entity - must be implemented by inheriting class.
*/
abstract public function sync_access();
/**
* Trigger a sync for all entities that this entity has access to (and recurse to group members).
* @param $seen used to prevent infinite recursion and double-syncing by tracking all entities seen so far
*/
public function sync_remote_access(&$seen = array()) {
$seen[$this->entity_id] = true;
// Sync whatever this entity has access to
$access_list = $this->list_remote_access();
foreach($access_list as $access) {
$access->dest_entity->sync_access();
}
// Sync whatever groups this entity is a member of
global $group_dir;
$memberships = $group_dir->list_group_membership($this);
foreach($memberships as $group) {
if(!isset($seen[$group->entity_id])) {
$group->sync_remote_access($seen);
}
}
// If this is a user, also sync across LDAP-based servers
global $server_dir;
global $sync_request_dir;
if(get_class($this) == 'User') {
$servers = $server_dir->list_servers(array(), array('authorization' => array('manual LDAP', 'automatic LDAP')));
foreach($servers as $server) {
$sync_request = new SyncRequest;
$sync_request->server_id = $server->id;
$sync_request->account_name = $this->uid;
$sync_request_dir->add_sync_request($sync_request);
}
}
}
}