Skip to content

Commit 864f8d3

Browse files
committed
auth - improve config revision auditability, closes #7033
This commit contains the following changes to improve revision visibility. * add username and api token for external (non-gui) callers. * offer the ability to merge revision information into configuration saves. (getRevisionContext / setRevisionContext) * merge session attributes starting with xrevision_ into a revision item, for example. xrevision_impersonated_by would be recorded as impersonated_by * add "impersonated_by" to audit log when specified (for future use) * remove revision attributes before adding, this prevents attributes sticking around.
1 parent 7f6ac2a commit 864f8d3

File tree

2 files changed

+81
-46
lines changed

2 files changed

+81
-46
lines changed

src/opnsense/mvc/app/controllers/OPNsense/Base/ApiControllerBase.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
namespace OPNsense\Base;
3030

3131
use OPNsense\Core\ACL;
32+
use OPNsense\Core\Config;
3233
use OPNsense\Auth\AuthenticationFactory;
3334

3435
/**
@@ -267,7 +268,11 @@ public function beforeExecuteRoute($dispatcher)
267268

268269
// link username on successful login
269270
$this->logged_in_user = $authResult['username'];
270-
271+
// pass revision context to config object
272+
Config::getInstance()->setRevisionContext([
273+
'username' => $authResult['username'],
274+
'user_apitoken' => $apiKey
275+
]);
271276
return true;
272277
}
273278
}

src/opnsense/mvc/app/library/OPNsense/Core/Config.php

Lines changed: 75 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,11 @@ class Config extends Singleton
6969
*/
7070
private $statusIsValid = false;
7171

72+
/**
73+
* @var array list of revision relevant data
74+
*/
75+
private $revisionContext = [];
76+
7277
/**
7378
* @var float current modification time of our known config
7479
*/
@@ -465,76 +470,101 @@ public function __toString()
465470
return $dom->saveXML();
466471
}
467472

473+
/**
474+
* @return array revision key/values
475+
*/
476+
public function getRevisionContext()
477+
{
478+
$revision = $this->revisionContext;
479+
$user_host = !empty($_SERVER['REMOTE_ADDR']) ? "@" . $_SERVER['REMOTE_ADDR'] : '';
480+
if (!empty($_SESSION["Username"])) {
481+
$revision['username'] = $_SESSION["Username"] . $user_host;
482+
} elseif (!isset($revision['username'])) {
483+
$revision['username'] = "(system)" . $user_host ;
484+
}
485+
$revision['description'] = sprintf(
486+
gettext('%s made changes'),
487+
!empty($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : $_SERVER['SCRIPT_NAME']
488+
);
489+
// append session revision tags when supplied (keys start with xrevision_)
490+
if (!empty($_SESSION) && is_array($_SESSION)) {
491+
foreach ($_SESSION as $key => $value) {
492+
if (stripos($key, 'xrevision_') === 0 && !isset($revision[substr($key,10)])) {
493+
$revision[substr($key,10)] = $value;
494+
}
495+
}
496+
497+
}
498+
$revision['time'] = empty($timestamp) ? microtime(true) : $timestamp;
499+
500+
return $revision;
501+
}
502+
503+
/**
504+
* set revision payload
505+
* @param array revision payload
506+
*/
507+
public function setRevisionContext($ctx)
508+
{
509+
if (is_array($ctx)) {
510+
$this->revisionContext = $ctx;
511+
return true;
512+
}
513+
return false;
514+
}
515+
468516
/**
469517
* update config revision information (ROOT.revision tag)
470518
* @param array|null $revision revision tag (associative array)
471519
* @param \SimpleXMLElement|null pass trough xml node
520+
* @return array revision data
472521
*/
473522
private function updateRevision($revision, $node = null, $timestamp = null)
474523
{
475-
// if revision info is not provided, create a default.
524+
/* If revision info is not provided, create one. $revision is used for recursion */
476525
if (!is_array($revision)) {
477-
$revision = array();
478-
// try to fetch userinfo from session
479-
if (!empty($_SESSION["Username"])) {
480-
$revision['username'] = $_SESSION["Username"];
481-
} else {
482-
$revision['username'] = "(system)";
483-
}
484-
if (!empty($_SERVER['REMOTE_ADDR'])) {
485-
$revision['username'] .= "@" . $_SERVER['REMOTE_ADDR'];
486-
}
487-
if (!empty($_SERVER['REQUEST_URI'])) {
488-
// when update revision is called from a controller, log the endpoint uri
489-
$revision['description'] = sprintf(gettext('%s made changes'), $_SERVER['REQUEST_URI']);
490-
} else {
491-
// called from a script, log script name and path
492-
$revision['description'] = sprintf(gettext('%s made changes'), $_SERVER['SCRIPT_NAME']);
493-
}
526+
$revision = $this->getRevisionContext();
494527
}
495-
496-
// always set timestamp
497-
$revision['time'] = empty($timestamp) ? microtime(true) : $timestamp;
498-
499528
if ($node == null) {
500-
if (isset($this->simplexml->revision)) {
501-
$node = $this->simplexml->revision;
529+
if (!isset($this->simplexml->revision)) {
530+
$target = $this->simplexml->addChild("revision");
502531
} else {
503-
$node = $this->simplexml->addChild("revision");
532+
$target = $this->simplexml->revision;
533+
foreach (iterator_to_array($target->children()) as $child) {
534+
unset($target->{$child->getName()});
535+
}
504536
}
537+
} else {
538+
$target = $node;
505539
}
506-
foreach ($revision as $revKey => $revItem) {
507-
if (isset($node->{$revKey})) {
508-
// key already in revision object
509-
$childNode = $node->{$revKey};
510-
} else {
511-
$childNode = $node->addChild($revKey);
512-
}
513-
if (is_array($revItem)) {
514-
$this->updateRevision($revItem, $childNode);
540+
541+
array_walk($revision, function($value, $key) use (&$target) {
542+
$node = $target->addChild($key);
543+
if (is_array($value)) {
544+
$this->updateRevision($value, $node);
515545
} else {
516-
$childNode[0] = $revItem;
546+
$node[0] = $value;
517547
}
518-
}
548+
});
549+
550+
return $revision;
519551
}
520552

521553
/**
522554
* send config change to audit log including the context we currently know of.
555+
* @param string $backup_filename new backup filename
556+
* @param array $revision revision adata used
523557
*/
524-
private function auditLogChange($backup_filename, $revision = null)
558+
private function auditLogChange($backup_filename, $revision)
525559
{
526560
openlog("audit", LOG_ODELAY, LOG_AUTH);
527-
$append_message = "";
528-
if (is_array($revision) && !empty($revision['description'])) {
529-
$append_message = sprintf(" [%s]", $revision['description']);
530-
}
531561
syslog(LOG_NOTICE, sprintf(
532562
"user %s%s changed configuration to %s in %s%s",
533-
!empty($_SESSION["Username"]) ? $_SESSION["Username"] : "(system)",
534-
!empty($_SERVER['REMOTE_ADDR']) ? "@" . $_SERVER['REMOTE_ADDR'] : "",
563+
$revision['username'],
564+
!empty($revision['impersonated_by']) ? sprintf(" (%s)", $revision['impersonated_by']) : '',
535565
$backup_filename,
536566
!empty($_SERVER['REQUEST_URI']) ? $_SERVER['REQUEST_URI'] : $_SERVER['SCRIPT_NAME'],
537-
$append_message
567+
$revision['description'] ?? ''
538568
));
539569
closelog();
540570
}
@@ -716,7 +746,7 @@ public function save($revision = null, $backup = true)
716746
$this->checkvalid();
717747
$time = microtime(true);
718748
// update revision information ROOT.revision tag, align timestamp to backup output
719-
$this->updateRevision($revision, null, $time);
749+
$revision = $this->updateRevision($revision, null, $time);
720750

721751
if ($this->config_file_handle !== null) {
722752
if (flock($this->config_file_handle, LOCK_EX)) {

0 commit comments

Comments
 (0)