mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-11-05 12:12:58 -05:00
Groups, Snapshots and Tags add/edit checked the ids from MonitorIds or
EventIds when the request had them, but Model::save() links the HABTM
entry (Monitor, Event) and ignores those fields on edit. A Groups=Edit
user could send {"Group":{"MonitorIds":"<current members>"},"Monitor":[]}:
the membership check saw no monitor moved, the save emptied the group and
lifted the Groups_Permissions deny on its monitors. On add, an empty
MonitorIds or EventIds next to a Monitor/Event list let monitors the user
may not edit into a new group, and events the user may not view into a
snapshot.
requestAssociatedIds() now reads the list the way save() does and
rewrites the request's entry to that plain list of ids, so a row given as
a join record links only its MonitorId/EventId. add() copies MonitorIds
or EventIds into the entry before the check, as it did before the save.
Test: tests/php/test_api_associated_ids.php
(cherry picked from commit 6bf015530e)
307 lines
13 KiB
PHP
307 lines
13 KiB
PHP
<?php
|
|
/**
|
|
* Application level Controller
|
|
*
|
|
* This file is application-wide controller file. You can put all
|
|
* application-wide controller-related methods here.
|
|
*
|
|
* CakePHP(tm) : Rapid Development Framework (https://cakephp.org)
|
|
* Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
|
|
*
|
|
* Licensed under The MIT License
|
|
* For full copyright and license information, please see the LICENSE.txt
|
|
* Redistributions of files must retain the above copyright notice.
|
|
*
|
|
* @copyright Copyright (c) Cake Software Foundation, Inc. (https://cakefoundation.org)
|
|
* @link https://cakephp.org CakePHP(tm) Project
|
|
* @package app.Controller
|
|
* @since CakePHP(tm) v 0.2.9
|
|
* @license https://www.opensource.org/licenses/mit-license.php MIT License
|
|
*/
|
|
App::uses('Controller', 'Controller');
|
|
App::uses('CrudControllerTrait', 'Crud.Lib');
|
|
|
|
/**
|
|
* Application Controller
|
|
*
|
|
* Add your application-wide methods in the class below, your controllers
|
|
* will inherit them.
|
|
*
|
|
* @package app.Controller
|
|
* @link https://book.cakephp.org/2.0/en/controllers.html#the-app-controller
|
|
*/
|
|
class AppController extends Controller {
|
|
use CrudControllerTrait;
|
|
|
|
public $components = [
|
|
'RequestHandler',
|
|
'Crud.Crud' => [
|
|
# No Crud actions are mapped. CrudControllerTrait would otherwise answer any action a
|
|
# controller does not define (view, add, edit, keyvalue, ...) with a generic handler
|
|
# that applies none of the controller's permission or per-monitor checks, e.g.
|
|
# zones/view/<id> returned any zone. A controller must implement each action itself.
|
|
'actions' => [],
|
|
'listeners' => ['Api', 'ApiTransformation']
|
|
#],
|
|
#'DebugKit.Toolbar' => [
|
|
# 'bootstrap' => true, 'routes' => true
|
|
]
|
|
];
|
|
|
|
// Global beforeFilter function
|
|
//Zoneminder sets the username session variable
|
|
// to the logged in user. If this variable is set
|
|
// then you are logged in
|
|
// its pretty simple to extend this to also check
|
|
// for role and deny API access in future
|
|
// Also checking to do this only if ZM_OPT_USE_AUTH is on
|
|
public function beforeFilter() {
|
|
if ( ! ZM_OPT_USE_API ) {
|
|
throw new UnauthorizedException(__('API Disabled'));
|
|
return;
|
|
}
|
|
|
|
# For use throughout the app. If not logged in, this will be null.
|
|
global $user;
|
|
require_once __DIR__ .'/../../../includes/auth.php';
|
|
# This will auto-login if username=&password= are set, or auth=
|
|
zm_authenticate_request();
|
|
|
|
if ( ZM_OPT_USE_AUTH ) {
|
|
if ( ZM_OPT_USE_LEGACY_API_AUTH or !strcasecmp($this->params->action, 'login') ) {
|
|
# This is here because historically we allowed user=&pass= in the api. web-ui auth uses username=&password=
|
|
$username = $this->request->query('user') ? $this->request->query('user') : $this->request->data('user');
|
|
$password = $this->request->query('pass') ? $this->request->query('pass') : $this->request->data('pass');
|
|
if ( $username and $password ) {
|
|
$ret = validateUser($username, $password);
|
|
$user = $ret[0];
|
|
$retstatus = $ret[1];
|
|
if ( !$user ) {
|
|
throw new UnauthorizedException(__($retstatus));
|
|
return;
|
|
}
|
|
ZM\Debug("Login successful for user \"$username\"");
|
|
migrateHash($username, $password);
|
|
}
|
|
}
|
|
|
|
if ( ZM_OPT_USE_LEGACY_API_AUTH ) {
|
|
require_once __DIR__ .'/../../../includes/session.php';
|
|
$stateful = $this->request->query('stateful') ? $this->request->query('stateful') : $this->request->data('stateful');
|
|
if ( $stateful ) {
|
|
// zm_session_start() already populates $_SESSION['remoteAddr'] from
|
|
// getRemoteAddr() (X-Forwarded-For via a trusted proxy), matching what
|
|
// getAuthUser() uses for validation. Don't overwrite it with bare
|
|
// REMOTE_ADDR here — that bound the hash to the proxy IP and broke
|
|
// validation behind a reverse proxy.
|
|
zm_session_start();
|
|
if ($user) {
|
|
if ((!empty($username) and !empty($password))
|
|
or empty($_SESSION['username']) or $_SESSION['username'] !== $user->Username()) {
|
|
// The session takes on a user it did not hold: a password login, or a
|
|
// stateful request with a token or an auth hash. A new session id, the
|
|
// old session dropped, so an id planted in the client beforehand (any
|
|
// anonymous request stores one) doesn't become this user's session
|
|
// (session fixation). This also stores the session.
|
|
zm_session_regenerate_id_login();
|
|
} else {
|
|
// The session already is this user's: keep it, and store it even though
|
|
// this request arrived without a cookie.
|
|
zm_session_persist();
|
|
}
|
|
$_SESSION['username'] = $user->Username();
|
|
// No password in the session: zmu gets a token (zmuCredentials()).
|
|
generateAuthHash(ZM_AUTH_HASH_IPS);
|
|
}
|
|
session_write_close();
|
|
} else if ( isset($_COOKIE['ZMSESSID']) and !$user ) {
|
|
# Have a cookie set, try to load user by session
|
|
if ( ! is_session_started() )
|
|
zm_session_start();
|
|
|
|
$user = userFromSession();
|
|
session_write_close();
|
|
}
|
|
}
|
|
|
|
# NON LEGACY, token based access
|
|
$token = $this->request->query('token') ? $this->request->query('token') : $this->request->data('token');
|
|
if ( $token ) {
|
|
// if you pass a token to login, we should only allow
|
|
// refresh tokens to regenerate new access and refresh tokens
|
|
if ( !strcasecmp($this->params->action, 'login') ) {
|
|
$only_allow_token_type = 'refresh';
|
|
} else {
|
|
// for any other methods, don't allow refresh tokens
|
|
// they are supposed to be infrequently used for security
|
|
// purposes
|
|
$only_allow_token_type = 'access';
|
|
}
|
|
$ret = validateToken($token, $only_allow_token_type, true);
|
|
$user = $ret[0];
|
|
$retstatus = $ret[1];
|
|
if ( !$user ) {
|
|
throw new UnauthorizedException(__($retstatus));
|
|
return;
|
|
}
|
|
} # end if token
|
|
|
|
if ( $user and ( $user->APIEnabled() != 1 ) ) {
|
|
ZM\Error('API disabled for: '.$user->Username());
|
|
throw new UnauthorizedException(__('API disabled for: '.$user->Username()));
|
|
$user = null;
|
|
}
|
|
|
|
// We need to reject methods that are not authenticated
|
|
// besides login and logout
|
|
if ( strcasecmp($this->params->action, 'logout') ) {
|
|
if ( !( $user and $user->Username() ) ) {
|
|
throw new UnauthorizedException(__('Not Authenticated'));
|
|
return;
|
|
} else if ( !( $user and $user->Enabled() ) ) {
|
|
throw new UnauthorizedException(__('User is not enabled'));
|
|
return;
|
|
}
|
|
} # end if ! login or logout
|
|
|
|
} # end if ZM_OPT_AUTH
|
|
// make sure populated user object has APIs enabled
|
|
|
|
if (isset($_SERVER['HTTP_ORIGIN'])) {
|
|
global $Servers;
|
|
if ( sizeof($Servers) < 1 ) {
|
|
# Only need CORSHeaders in the event that there are multiple servers in use.
|
|
# ICON: Might not be true. multi-port?
|
|
if ( ZM_MIN_STREAMING_PORT ) {
|
|
ZM\Debug('Setting default Access-Control-Allow-Origin from ' . $_SERVER['HTTP_ORIGIN']);
|
|
$this->response->header('Access-Control-Allow-Origin: ' . $_SERVER['HTTP_ORIGIN']);
|
|
$this->response->header('Access-Control-Allow-Credentials: true');
|
|
$this->response->header('Access-Control-Allow-Headers: x-requested-with,x-request');
|
|
}
|
|
return;
|
|
}
|
|
foreach ($Servers as $Server) {
|
|
if (
|
|
preg_match('/^(https?:\/\/)?'.preg_quote($Server->Hostname(),'/').'/i', $_SERVER['HTTP_ORIGIN'])
|
|
or
|
|
preg_match('/^(https?:\/\/)?'.preg_quote($Server->Name(),'/').'/i', $_SERVER['HTTP_ORIGIN'])
|
|
) {
|
|
ZM\Debug('Setting Access-Control-Allow-Origin from '.$_SERVER['HTTP_ORIGIN']);
|
|
$this->response->header('Access-Control-Allow-Origin: ' . $_SERVER['HTTP_ORIGIN']);
|
|
$this->response->header('Access-Control-Allow-Credentials: true');
|
|
$this->response->header('Access-Control-Allow-Headers: x-requested-with,x-request');
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
} # end function beforeFilter()
|
|
|
|
# Model::save() takes the record id from a primary key in the data it is given, so an
|
|
# edit authorized for the id in the URL could otherwise write to whichever id the request
|
|
# body names. Drop any primary key from the request data and pin the model to $id.
|
|
protected function pinRequestId($model, $id) {
|
|
$alias = $model->alias;
|
|
$key = $model->primaryKey;
|
|
if (isset($this->request->data[$alias]) and is_array($this->request->data[$alias])) {
|
|
unset($this->request->data[$alias][$key]);
|
|
}
|
|
unset($this->request->data[$key]);
|
|
$model->id = $id;
|
|
}
|
|
|
|
# A find() condition limiting $field to the monitors the user may view: empty when the user
|
|
# is not restricted, and matching nothing when the user may view no monitor at all.
|
|
protected function viewableMonitorCondition($field) {
|
|
global $user;
|
|
if (!$user or !$user->unviewableMonitorIds()) return array();
|
|
$ids = $user->viewableMonitorIds();
|
|
return array($field => count($ids) ? $ids : array(0));
|
|
}
|
|
|
|
# A field from the request data, or null, read the way Model::set() will save it: from
|
|
# Model[field] when the request has a non-empty Model entry, otherwise from the bare field.
|
|
# Reading Model[field] whenever Model was an array let {"Monitor":[],"EventEndCommand":"x"}
|
|
# pass a check that found no value while save() stored x. refs GHSA-fp33-8fx5-4j6c
|
|
protected function requestField($alias, $field) {
|
|
$data = $this->request->data;
|
|
if (!empty($data[$alias])) {
|
|
$data = is_array($data[$alias]) ? $data[$alias] : array();
|
|
}
|
|
return isset($data[$field]) ? $data[$field] : null;
|
|
}
|
|
|
|
# Throw unless the user may view monitor $monitorId.
|
|
protected function requireMonitorView($monitorId) {
|
|
require_once __DIR__ .'/../../../includes/Monitor.php';
|
|
$monitor = new ZM\Monitor($monitorId);
|
|
if (!$monitor->Id()) {
|
|
throw new NotFoundException(__('Invalid monitor'));
|
|
}
|
|
if (!$monitor->canView()) {
|
|
throw new UnauthorizedException(__('Insufficient Privileges'));
|
|
}
|
|
}
|
|
|
|
# Throw unless the user may edit monitor $monitorId.
|
|
protected function requireMonitorEdit($monitorId) {
|
|
require_once __DIR__ .'/../../../includes/Monitor.php';
|
|
$monitor = new ZM\Monitor($monitorId);
|
|
if (!$monitor->Id()) {
|
|
throw new NotFoundException(__('Invalid monitor'));
|
|
}
|
|
if (!$monitor->canEdit()) {
|
|
throw new UnauthorizedException(__('Insufficient Privileges'));
|
|
}
|
|
}
|
|
|
|
# The ids of the $assoc records a save of the request links to $alias through its HABTM
|
|
# association, read the way Model::save() reads them, or null when the save leaves the links
|
|
# alone. Model::set() skips a non-array $assoc entry; save() takes $assoc[$assoc] when present,
|
|
# else $assoc, and replaces all links with it. $alias[EventIds]-style fields are not saved
|
|
# (add() copies them to $assoc first). The entry is rewritten to the plain list returned, so a
|
|
# row given as a join record, which save() would store as given, can only link the id checked.
|
|
protected function requestAssociatedIds($alias, $assoc) {
|
|
if (!isset($this->request->data[$assoc])) return null;
|
|
$rows = $this->request->data[$assoc];
|
|
if (!is_array($rows)) {
|
|
unset($this->request->data[$assoc]);
|
|
return null;
|
|
}
|
|
if (isset($rows[$assoc])) $rows = $rows[$assoc];
|
|
$key = $this->{$alias}->hasAndBelongsToMany[$assoc]['associationForeignKey'];
|
|
$ids = array();
|
|
foreach ((array)$rows as $row) {
|
|
if (is_array($row)) $row = isset($row[$key]) ? $row[$key] : null;
|
|
if (is_numeric($row)) $ids[] = intval($row);
|
|
}
|
|
$this->request->data[$assoc] = array($assoc => $ids);
|
|
return $ids;
|
|
}
|
|
|
|
# Throw unless the user may view every event in $eventIds, which includes its monitor.
|
|
protected function requireEventsView($eventIds) {
|
|
require_once __DIR__ .'/../../../includes/Event.php';
|
|
foreach (array_unique($eventIds) as $eventId) {
|
|
$event = new ZM\Event($eventId);
|
|
if (!$event->Id()) {
|
|
throw new NotFoundException(__('Invalid event'));
|
|
}
|
|
if (!$event->canView()) {
|
|
throw new UnauthorizedException(__('Insufficient Privileges'));
|
|
}
|
|
}
|
|
}
|
|
|
|
# Drop the events on monitors the user may not view from a row's contained Event list.
|
|
protected function filterContainedEvents($row, $key = 'Event') {
|
|
$condition = $this->viewableMonitorCondition('MonitorId');
|
|
if (!count($condition) or !isset($row[$key])) return $row;
|
|
$allowed = $condition['MonitorId'];
|
|
$row[$key] = array_values(array_filter($row[$key], function($event) use ($allowed) {
|
|
return isset($event['MonitorId']) and in_array($event['MonitorId'], $allowed);
|
|
}));
|
|
return $row;
|
|
}
|
|
}
|