1<?php
2##
3## Copyright 2013-2018 Opera Software AS
4##
5## Licensed under the Apache License, Version 2.0 (the "License");
6## you may not use this file except in compliance with the License.
7## You may obtain a copy of the License at
8##
9## http://www.apache.org/licenses/LICENSE-2.0
10##
11## Unless required by applicable law or agreed to in writing, software
12## distributed under the License is distributed on an "AS IS" BASIS,
13## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14## See the License for the specific language governing permissions and
15## limitations under the License.
16##
17
18/**
19* Class that represents a user of this system.
20*/
21class User extends Record {
22	/**
23	* Defines the database table that this object is stored in
24	*/
25	protected $table = 'user';
26	/**
27	* LDAP connection object
28	*/
29	private $ldap;
30
31	public function __construct($id = null, $preload_data = array()) {
32		parent::__construct($id, $preload_data);
33		global $ldap;
34		$this->ldap = $ldap;
35	}
36
37	/**
38	* Add an alert to be displayed to this user on their next normal page load.
39	* @param UserAlert $alert to be displayed
40	*/
41	public function add_alert(UserAlert $alert) {
42		if(is_null($this->id)) throw new BadMethodCallException('User must be in directory before alerts can be added');
43		$stmt = $this->database->prepare('INSERT INTO user_alert (user_id, class, content, escaping) VALUES (?, ?, ?, ?)');
44		$stmt->bindParam(1, $this->id, PDO::PARAM_INT);
45		$stmt->bindParam(2, $alert->class, PDO::PARAM_STR);
46		$stmt->bindParam(3, $alert->content, PDO::PARAM_STR);
47		$stmt->bindParam(4, $alert->escaping, PDO::PARAM_INT);
48		$stmt->execute();
49		$alert->id = $this->database->lastInsertId('user_alert_id_seq');
50	}
51
52	/**
53	* List all alerts for this user *and* delete them.
54	* @return array of UserAlert objects
55	*/
56	public function pop_alerts() {
57		if(is_null($this->id)) throw new BadMethodCallException('User must be in directory before alerts can be listed');
58		$stmt = $this->database->prepare('SELECT * FROM user_alert WHERE user_id = ?');
59		$stmt->bindParam(1, $this->id, PDO::PARAM_INT);
60		$stmt->execute();
61		$alerts = array();
62		$alert_ids = array();
63		while($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
64			$alerts[] = new UserAlert($row['id'], $row);
65			$alert_ids[] = $row['id'];
66		}
67		if(count($alert_ids) > 0) {
68			$this->database->query('DELETE FROM user_alert WHERE id IN ('.implode(', ', $alert_ids).')');
69		}
70		return $alerts;
71	}
72
73	/**
74	* Return HTML containing this user's CSRF token for inclusion in a POST form.
75	* Also includes a random string of the same length to help guard against http://breachattack.com/
76	* @return string HTML
77	*/
78	public function get_csrf_field() {
79		return '<input type="hidden" name="csrf_token" value="'.hesc($this->get_csrf_token()).'"><!-- '.hash("sha512", mt_rand(0, mt_getrandmax())).' -->'."\n";
80	}
81
82	/**
83	* Return this user's CSRF token. Generate one if they do not yet have one.
84	* @return string CSRF token
85	*/
86	public function get_csrf_token() {
87		if(is_null($this->id)) throw new BadMethodCallException('User must be in directory before CSRF token can be generated');
88		if(!isset($this->data['csrf_token'])) {
89			$this->data['csrf_token'] = hash("sha512", mt_rand(0, mt_getrandmax()));
90			$this->update();
91		}
92		return $this->data['csrf_token'];
93	}
94
95	/**
96	* Check the given string against this user's CSRF token.
97	* @return bool true on string match
98	*/
99	public function check_csrf_token($token) {
100		return $token === $this->get_csrf_token();
101	}
102
103	/**
104	* Retrieve this user's details from the configured data source.
105	* @throws UserDataSourceException if no user data source is configured
106	*/
107	public function get_details() {
108		global $config;
109		if(!empty($config['ldap']['enabled'])) {
110			$this->get_details_from_ldap();
111		} elseif(!empty($config['php_auth']['enabled'])) {
112			$this->get_details_from_php_auth();
113		} else {
114			throw new UserDataSourceException('User data source not configured.');
115		}
116	}
117
118	/**
119	* Retrieve this user's details from PHP_AUTH variables.
120	* @throws UserNotFoundException if the user details are not found in PHP_AUTH variables
121	*/
122	public function get_details_from_php_auth() {
123		global $config;
124		if($this->uid == $_SERVER['PHP_AUTH_USER'] and isset($_SERVER['PHP_AUTH_NAME']) and isset($_SERVER['PHP_AUTH_EMAIL']) and isset($_SERVER['PHP_AUTH_GROUPS'])) {
125			$this->auth_realm = 'PHP_AUTH';
126			$this->name = $_SERVER['PHP_AUTH_NAME'];
127			$this->email = $_SERVER['PHP_AUTH_EMAIL'];
128			$this->active = 1;
129			$this->admin = 0;
130			$groups = explode(' ', $_SERVER['PHP_AUTH_GROUPS']);
131			foreach($groups as $group) {
132				if($group == $config['php_auth']['admin_group']) $this->admin = 1;
133			}
134		} else {
135			throw new UserNotFoundException('User does not exist in PHP_AUTH variables.');
136		}
137	}
138
139	/**
140	* Retrieve this user's details from LDAP.
141	* @throws UserNotFoundException if the user is not found in LDAP
142	*/
143	public function get_details_from_ldap() {
144		global $config;
145		$attributes = array();
146		$attributes[] = 'dn';
147		$attributes[] = $config['ldap']['user_id'];
148		$attributes[] = $config['ldap']['user_name'];
149		$attributes[] = $config['ldap']['user_email'];
150		$attributes[] = $config['ldap']['group_member_value'];
151		if(isset($config['ldap']['user_active'])) {
152			$attributes[] = $config['ldap']['user_active'];
153		}
154		$ldapusers = $this->ldap->search($config['ldap']['dn_user'], LDAP::escape($config['ldap']['user_id']).'='.LDAP::escape($this->uid), array_keys(array_flip($attributes)));
155		if($ldapuser = reset($ldapusers)) {
156			$this->auth_realm = 'LDAP';
157			$this->uid = $ldapuser[strtolower($config['ldap']['user_id'])];
158			$this->name = $ldapuser[strtolower($config['ldap']['user_name'])];
159			$this->email = $ldapuser[strtolower($config['ldap']['user_email'])];
160			if(isset($config['ldap']['user_active'])) {
161				$this->active = 0;
162				if(isset($config['ldap']['user_active_true'])) {
163					$this->active = intval($ldapuser[strtolower($config['ldap']['user_active'])] == $config['ldap']['user_active_true']);
164				} elseif(isset($config['ldap']['user_active_false'])) {
165					$this->active = intval($ldapuser[strtolower($config['ldap']['user_active'])] != $config['ldap']['user_active_false']);
166				}
167			} else {
168				$this->active = 1;
169			}
170			$this->admin = 0;
171			$group_member = $ldapuser[strtolower($config['ldap']['group_member_value'])];
172			$ldapgroups = $this->ldap->search($config['ldap']['dn_group'], LDAP::escape($config['ldap']['group_member']).'='.LDAP::escape($group_member), array('cn'));
173			foreach($ldapgroups as $ldapgroup) {
174				if($ldapgroup['cn'] == $config['ldap']['admin_group_cn']) $this->admin = 1;
175			}
176		} else {
177			throw new UserNotFoundException('User does not exist in LDAP.');
178		}
179	}
180
181	/**
182	* Return the access level of this user to the specified zone.
183	* @param Zone $zone to check for access
184	* @return string name of access level
185	*/
186	public function access_to(Zone $zone) {
187		$stmt = $this->database->prepare('SELECT level FROM zone_access WHERE user_id = ? AND zone_id = ?');
188		$stmt->bindParam(1, $this->id, PDO::PARAM_INT);
189		$stmt->bindParam(2, $zone->id, PDO::PARAM_INT);
190		$stmt->execute();
191		if($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
192			return $row['level'];
193		} else {
194			return false;
195		}
196	}
197
198	/**
199	* List all zones that this user is an administrator of
200	* @param array $include list of extra data to include in response
201	* @return array of Zone objects
202	*/
203	public function list_admined_zones($include = array()) {
204		global $zone_dir;
205		$zones = $zone_dir->list_zones($include);
206		$admined_zones = array();
207		foreach($zones as $zone) {
208			if($this->access_to($zone)) $admined_zones[$zone->pdns_id] = $zone;
209		}
210		return $admined_zones;
211	}
212
213	/**
214	* List all zones that this user has access to in some way
215	* @param array $include list of extra data to include in response
216	* @return array of Zone objects
217	*/
218	public function list_accessible_zones($include = array()) {
219		global $zone_dir;
220		if($this->admin) {
221			$zones = $zone_dir->list_zones($include);
222		} else {
223			$zones = $this->list_admined_zones($include);
224		}
225		return $zones;
226	}
227
228	/**
229	* List all changes that this user has made to any zones
230	* @return array of ChangeSet objects
231	*/
232	public function list_changesets() {
233		global $user_dir;
234		$stmt = $this->database->prepare('
235		SELECT changeset.*, zone.pdns_id, zone.name, zone.serial, zone.account, zone.active
236		FROM changeset
237		INNER JOIN zone ON zone.id = changeset.zone_id
238		WHERE author_id = ?
239		ORDER BY id DESC
240		');
241		$stmt->bindParam(1, $this->id, PDO::PARAM_INT);
242		$stmt->execute();
243		$changesets = array();
244		while($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
245			$row['author'] = $this;
246			$row['requester'] = (is_null($row['requester_id']) ? null : $user_dir->get_user_by_id($row['requester_id']));
247			$row['zone'] = new Zone($row['id'], array('pdns_id' => $row['pdns_id'], 'name' => $row['name'], 'serial' => $row['serial'], 'account' => $row['account'], 'active' => $row['active']));
248			unset($row['pdns_id']);
249			unset($row['name']);
250			unset($row['serial']);
251			unset($row['account']);
252			unset($row['active']);
253			$row['change_date'] = DateTime::createFromFormat('Y-m-d H:i:s.u', $row['change_date']);
254			$changesets[] = new ChangeSet($row['id'], $row);
255		}
256		return $changesets;
257	}
258}
259