YOURLS-AuthMgrPlus/authMgrPlus/plugin.php
Marcos de Oliveira e584626e5e Fix default role to permit less restrictive
* Fix some spaces indentation
	* Create $amp_role_assigned variable

Before this change, if amp_default_role was less restrictive than the
one assigned to user, user assigned role had no effect at all. This
change permit the default role to be less restrictive than users's.
2021-04-05 11:01:33 -03:00

629 lines
18 KiB
PHP

<?php
/*
Plugin Name: Auth Manager Plus
Plugin URI: https://github.com/joshp23/YOURLS-AuthMgrPlus
Description: Role Based Access Controlls with seperated user data for authenticated users
Version: 2.2.6
Author: Josh Panter, nicwaller, Ian Barber <ian.barber@gmail.com>
Author URI: https://unfettered.net
*/
// No direct call
if( !defined( 'YOURLS_ABSPATH' ) ) die();
/****************** SET UP CONSTANTS ******************/
class ampRoles {
const Administrator = 'Administrator';
const Editor = 'Editor';
const Contributor = 'Contributor';
}
class ampCap {
const ShowAdmin = 'ShowAdmin';
const AddURL = 'AddURL';
const DeleteURL = 'DeleteURL';
const EditURL = 'EditURL';
const ShareURL = 'ShareURL';
const Traceless = 'Traceless';
const ManageAnonURL = 'ManageAnonURL';
const ManageUsrsURL = 'ManageUsrsURL';
const ManagePlugins = 'ManagePlugins';
const API = 'API';
const APIu = 'APIu';
const ViewStats = 'ViewStats';
const ViewAll = 'ViewAll';
}
/********** Add hooks to intercept functionality in CORE **********/
yourls_add_action( 'load_template_infos', 'amp_intercept_stats' );
function amp_intercept_stats() {
if ( 'YOURLS_PRIVATE_INFOS' === true ) {
amp_require_capability( ampCap::ViewStats );
}
}
yourls_add_action( 'api', 'amp_intercept_api' );
function amp_intercept_api() {
if ( 'YOURLS_PRIVATE_API' === true ) {
if ( isset( $_REQUEST['shorturl'] ) || isset( $_REQUEST['stats'] ) ) {
amp_require_capability( ampCap::APIu );
} else {
amp_require_capability( ampCap::API );
}
}
}
yourls_add_action( 'auth_successful', function() {
if( yourls_is_admin() ) amp_intercept_admin();
} );
/**
* YOURLS processes most actions in the admin page. It would be ideal
* to add a unique hook for each action, but unfortunately we need to
* hook the admin page load itself, and try to figure out what action
* is intended.
*
* TODO: look for these hooks
*
* At this point, reasonably assume that the current request is for
* a rendering of the admin page.
*/
function amp_intercept_admin() {
amp_require_capability( ampCap::ShowAdmin );
// we use this GET param to send up a feedback notice to user
if ( isset( $_GET['access'] ) && $_GET['access']=='denied' ) {
yourls_add_notice('Access Denied');
}
// allow manipulation of this list ( be mindfull of extending Auth mp Capability class if needed )
$action_capability_map = yourls_apply_filter( 'amp_action_capability_map',
array( 'add' => ampCap::AddURL,
'delete' => ampCap::DeleteURL,
'edit_display' => ampCap::EditURL,
'edit_save' => ampCap::EditURL,
'activate' => ampCap::ManagePlugins,
'deactivate' => ampCap::ManagePlugins,
) );
// Key actions like Add/Edit/Delete are AJAX requests
if ( yourls_is_Ajax() ) {
// Define some boundaries for ownership
// Allow some flexability with those boundaries
$restricted_actions = yourls_apply_filter( 'amp_restricted_ajax_actions',
array( 'edit_display',
'edit_save',
'delete'
) );
$action_keyword = $_REQUEST['action'];
$cap_needed = $action_capability_map[$action_keyword];
// Check the action against those boundaries
if ( in_array( $action_keyword, $restricted_actions) ) {
$keyword = $_REQUEST['keyword'];
$do = amp_manage_keyword( $keyword, $cap_needed );
} else {
$do = amp_have_capability( $cap_needed );
}
if ( $do !== true ) {
$err = array();
$err['status'] = 'fail';
$err['code'] = 'error:authorization';
$err['message'] = 'Access Denied';
$err['errorCode'] = '403';
echo json_encode( $err );
die();
}
}
// Intercept requests for plugin management
if( isset( $_SERVER['REQUEST_URI'] ) && preg_match('/\/admin\/plugins\.php.*/', $_SERVER['REQUEST_URI'] ) ) {
// Is this a plugin page request?
if ( isset( $_REQUEST['page'] ) ) {
// Is this an allowed plugin?
global $amp_allowed_plugin_pages;
if ( amp_have_capability( ampCap::ManagePlugins ) !== true) {
$r = $_REQUEST['page'];
if(!in_array($r, $amp_allowed_plugin_pages ) ) {
yourls_redirect( yourls_admin_url( '?access=denied' ), 302 );
}
}
} else {
// Should this user touch plugins?
if ( amp_have_capability( ampCap::ManagePlugins ) !== true) {
yourls_redirect( yourls_admin_url( '?access=denied' ), 302 );
}
}
// intercept requests for global plugin management actions
if (isset( $_REQUEST['plugin'] ) ) {
$action_keyword = $_REQUEST['action'];
$cap_needed = $action_capability_map[$action_keyword];
if ( $cap_needed !== NULL && amp_have_capability( $cap_needed ) !== true) {
yourls_redirect( yourls_admin_url( '?access=denied' ), 302 );
}
}
}
}
/*
* Cosmetic filter: removes disallowed buttons from link list per short link
*/
yourls_add_filter( 'table_add_row_action_array', 'amp_ajax_button_check' );
function amp_ajax_button_check( $actions, $keyword ) {
// define the amp capabilities that map to the buttons
$button_cap_map = array('stats' => ampCap::ViewStats,
'share' => ampCap::ShareURL,
'edit' => ampCap::EditURL,
'delete' => ampCap::DeleteURL,
);
$button_cap_map = yourls_apply_filter( 'amp_button_capability_map', $button_cap_map );
// define restricted buttons
$restricted_buttons = array('delete', 'edit');
if ( 'YOURLS_PRIVATE_INFOS' === true )
$restricted_buttons += ['stats'];
$restricted_buttons = yourls_apply_filter( 'amp_restricted_buttons', $restricted_buttons );
// unset any disallowed buttons
foreach ( $actions as $action => $vars ) {
$cap_needed = $button_cap_map[$action];
if ( in_array( $action, $restricted_buttons) )
$show = amp_manage_keyword( $keyword, $cap_needed );
else
$show = amp_have_capability( $cap_needed );
if (!$show)
unset( $actions[$action] );
}
return $actions;
}
/*
* Cosmetic filter: removes disallowed plugins from link list
*/
yourls_add_filter( 'admin_sublinks', 'amp_admin_sublinks' );
function amp_admin_sublinks( $links ) {
global $amp_allowed_plugin_pages;
if( empty( $links['plugins'] ) ) {
unset($links['plugins']);
} else {
if ( amp_have_capability( ampCap::ManagePlugins ) !== true) {
foreach( $links['plugins'] as $link => $ar ) {
if(!in_array($link, $amp_allowed_plugin_pages) )
unset($links['plugins'][$link]);
}
}
sort($links['plugins']);
}
return $links;
}
/*
* Cosmetic filter: displays currently available roles
* by hovering mouse over the username in logout link.
*/
yourls_add_filter( 'logout_link', 'amp_html_append_roles' );
function amp_html_append_roles( $original ) {
if ( amp_is_valid_user() ) {
$listcaps = implode(', ', amp_current_capabilities());
return '<div title="'.$listcaps.'">'.$original.'</div>';
} else {
return $original;
}
}
/**************** CAPABILITY TESTING ****************/
/*
* If capability is not permitted in current context, then abort.
* This is the most basic way to intercept unauthorized usage.
*/
// TODO: API responses!
function amp_require_capability( $capability ) {
if ( !amp_have_capability( $capability ) ) {
// If the user can't view admin interface, return a plain error.
if ( !amp_have_capability( ampCap::ShowAdmin ) ) {
// header("HTTP/1.0 403 Forbidden");
die('Require permissions to show admin interface.');
}
// Otherwise, render errors in admin interface
yourls_redirect( yourls_admin_url( '?access=denied' ), 302 );
die();
}
}
// Heart of system - Can the user do "X"?
function amp_have_capability( $capability ) {
global $amp_anon_capabilities;
global $amp_role_capabilities;
global $amp_admin_ipranges;
global $amp_default_role;
// Make sure the environment has been setup
amp_env_check();
// Check anon capabilities
$return = in_array( $capability, $amp_anon_capabilities );
// Check user-role based auth
if( !$return ) {
// Only users have roles
if ( !amp_is_valid_user() ) //XXX
return false;
// List capabilities of particular user role
$user = YOURLS_USER !== false ? YOURLS_USER : NULL;
$user_caps = array();
foreach ( $amp_role_capabilities as $rolename => $rolecaps ) {
if ( amp_user_has_role( $user, $rolename ) ) {
$amp_role_assigned = True;
$user_caps = array_merge( $user_caps, $rolecaps );
}
}
$user_caps = array_unique( $user_caps );
// Is the requested capability in this list?
$return = in_array( $capability, $user_caps );
}
// Is user connecting from an admin designated IP?
if( !$return ) {
// the array of ranges: '127.0.0.0/8' will always be admin
foreach ($amp_admin_ipranges as $range) {
$return = amp_cidr_match( $_SERVER['REMOTE_ADDR'], $range );
if( $return )
break;
}
}
if( !$amp_role_assigned ) {
if ( isset( $amp_default_role ) && in_array ($amp_default_role, array_keys( $amp_role_capabilities ) ) ) {
$default_caps = $amp_role_capabilities [ $amp_default_role ];
$return = in_array( $capability, $default_caps );
}
}
return $return;
}
// Determine whether a specific user has a role.
function amp_user_has_role( $username, $rolename ) {
global $amp_role_assignment;
// if no role assignments are created, grant everything FIXME: Make 'admin'
// so the site still works even if stuff is configured wrong
if ( empty( $amp_role_assignment ) )
return true;
// do this the case-insensitive way
// the entire array was made lowercase in environment check
$username = strtolower($username);
$rolename = strtolower($rolename);
// if the role doesn't exist, give up now.
if ( !in_array( $rolename, array_keys( $amp_role_assignment ) ) )
return false;
$users_in_role = $amp_role_assignment[$rolename];
return in_array( $username, $users_in_role );
}
/********************* KEYWORD OWNERSHIP ************************/
// Filter out restricted access to keyword data in...
// Admin list
yourls_add_filter( 'admin_list_where', 'amp_admin_list_where' );
function amp_admin_list_where($where) {
if ( amp_have_capability( ampCap::ViewAll ) )
return $where; // Allow admin/editor users to see the lot.
$user = YOURLS_USER !== false ? YOURLS_USER : NULL;
$where['sql'] = $where['sql'] . " AND (`user` = :user OR `user` IS NULL) ";
$where['binds']['user'] = $user;
return $where;
}
// API stats
yourls_add_filter( 'api_url_stats', 'amp_api_url_stats' );
function amp_api_url_stats( $return, $shorturl ) {
$keyword = str_replace( YOURLS_SITE . '/' , '', $shorturl ); // accept either 'http://ozh.in/abc' or 'abc'
$keyword = yourls_sanitize_string( $keyword );
$keyword = addslashes($keyword);
if( ( !defined('YOURLS_PRIVATE_INFOS') || YOURLS_PRIVATE_INFOS !== false )
&& !amp_access_keyword($keyword) )
return array('simple' => "URL is owned by another user", 'message' => 'URL is owned by another user', 'errorCode' => 403);
else
return $return;
}
// Info pages
yourls_add_action( 'pre_yourls_infos', 'amp_pre_yourls_infos' );
function amp_pre_yourls_infos( $keyword ) {
if( yourls_is_private() && !amp_access_keyword($keyword) ) {
if ( !amp_is_valid_user() )
yourls_redirect( yourls_admin_url( '?access=denied' ), 302 );
else
yourls_redirect( YOURLS_SITE, 302 );
}
}
// DB stats
yourls_add_filter( 'get_db_stats', 'amp_get_db_stats' );
function amp_get_db_stats( $return, $where ) {
if ( amp_have_capability( ampCap::ViewAll ) )
return $return; // Allow admin/editor users to see the lot.
// or... filter results
global $ydb;
$table_url = YOURLS_DB_TABLE_URL;
$user = YOURLS_USER !== false ? YOURLS_USER : NULL;
$where['sql'] = $where['sql'] . " AND (`user` = :user OR `user` IS NULL) ";
$where['binds']['user'] = $user;
$sql = "SELECT COUNT(keyword) as count, SUM(clicks) as sum FROM `$table_url` WHERE 1=1 " . $where['sql'];
$binds = $where['binds'];
$totals = $ydb->fetchObject($sql, $binds);
$return = array( 'total_links' => $totals->count, 'total_clicks' => $totals->sum );
return $return;
}
// Fine tune track-me-not
yourls_add_action('redirect_shorturl', 'amp_tracking');
function amp_tracking( $u, $k = false ) {
if( amp_is_valid_user() && ( amp_keyword_owner($k) || amp_have_capability( ampCap::Traceless ) ) ) {
// No logging
yourls_add_filter( 'shunt_update_clicks', function( ) { return true; } );
yourls_add_filter( 'shunt_log_redirect', function( ) { return true; } );
}
}
/********************* HOUSEKEEPING ************************/
// Validate environment setup
function amp_env_check() {
global $amp_anon_capabilities;
global $amp_role_capabilities;
global $amp_role_assignment;
global $amp_admin_ipranges;
global $amp_allowed_plugin_pages;
if ( !isset( $amp_anon_capabilities) ) {
$amp_anon_capabilities = array();
}
if ( !isset( $amp_role_capabilities) ) {
$amp_role_capabilities = array(
ampRoles::Administrator => array(
ampCap::ShowAdmin,
ampCap::AddURL,
ampCap::EditURL,
ampCap::DeleteURL,
ampCap::ShareURL,
ampCap::Traceless,
ampCap::ManageAnonURL,
ampCap::ManageUsrsURL,
ampCap::ManagePlugins,
ampCap::API,
ampCap::APIu,
ampCap::ViewStats,
ampCap::ViewAll,
),
ampRoles::Editor => array(
ampCap::ShowAdmin,
ampCap::AddURL,
ampCap::EditURL,
ampCap::DeleteURL,
ampCap::ShareURL,
ampCap::Traceless,
ampCap::ManageAnonURL,
ampCap::APIu,
ampCap::ViewStats,
ampCap::ViewAll,
),
ampRoles::Contributor => array(
ampCap::ShowAdmin,
ampCap::AddURL,
ampCap::EditURL,
ampCap::DeleteURL,
ampCap::ShareURL,
ampCap::APIu,
ampCap::ViewStats,
),
);
}
if ( !isset( $amp_role_assignment ) ) {
$amp_role_assignment = array();
}
if ( !isset( $amp_admin_ipranges ) ) {
$amp_admin_ipranges = array(
'127.0.0.0/8',
);
}
if ( !isset( $amp_allowed_plugin_pages ) ) {
$amp_allowed_plugin_pages = array(
);
}
// convert role assignment table to lower case if it hasn't been done already
// this makes searches much easier!
$amp_role_assignment_lower = array();
foreach ( $amp_role_assignment as $key => $value ) {
$t_key = strtolower( $key );
$t_value = array_map('strtolower', $value);
$amp_role_assignment_lower[$t_key] = $t_value;
}
$amp_role_assignment = $amp_role_assignment_lower;
unset($amp_role_assignment_lower);
return true;
}
// Activation: add the user column to the URL table if not added
yourls_add_action( 'activated_authMgrPlus/plugin.php', 'amp_activated' );
function amp_activated() {
global $ydb;
$table = YOURLS_DB_TABLE_URL;
$sql = "DESCRIBE `".$table."`";
$results = $ydb->fetchObjects($sql);
$activated = false;
foreach($results as $r) {
if($r->Field == 'user') {
$activated = true;
}
}
if(!$activated) {
if ($version) {
$sql = "ALTER TABLE `".$table."` ADD `user` VARCHAR(255) NULL";
$insert = $ydb->fetchAffected($sql);
} else {
$ydb->query("ALTER TABLE `".$table."` ADD `user` VARCHAR(255) NULL");
}
}
}
/***************** HELPER FUNCTIONS ********************/
// List currently available capabilities
function amp_current_capabilities() {
$current_capabilities = array();
$all_capabilities = array(
ampCap::ShowAdmin,
ampCap::AddURL,
ampCap::EditURL,
ampCap::DeleteURL,
ampCap::ShareURL,
ampCap::Traceless,
ampCap::ManageAnonURL,
ampCap::ManageUsrsURL,
ampCap::ManagePlugins,
ampCap::API,
ampCap::APIu,
ampCap::ViewStats,
ampCap::ViewAll,
);
foreach ( $all_capabilities as $cap ) {
if ( amp_have_capability( $cap ) ) {
$current_capabilities[] = $cap;
}
}
// allow manipulation of this list ( be mindfull of extending the ampCap class if needed )
$current_capabilities = yourls_apply_filter( 'amp_current_capabilities', $current_capabilities);
return $current_capabilities;
}
// Check for IP in a range
// from: http://stackoverflow.com/questions/594112/matching-an-ip-to-a-cidr-mask-in-php5
function amp_cidr_match($ip, $range) {
list ($subnet, $bits) = explode('/', $range);
$ip = ip2long($ip);
$subnet = ip2long($subnet);
$mask = -1 << (32 - $bits);
$subnet &= $mask; # nb: in case the supplied subnet wasn't correctly aligned
return ($ip & $mask) == $subnet;
}
// Check user access to a keyword ( can they see it )
function amp_access_keyword( $keyword ) {
$users = array( YOURLS_USER !== false ? YOURLS_USER : NULL , NULL );
$owner = amp_keyword_owner( $keyword );
if ( amp_have_capability( ampCap::ViewAll ) || in_array( $owner , $users ) )
return true;
}
// Check user rights to a keyword ( can manage it )
function amp_manage_keyword( $keyword, $capability ) {
$return = false; // default is to deny access
if ( amp_is_valid_user() ) { // only authenticated users can manaage keywords
$owner = amp_keyword_owner($keyword);
$user = YOURLS_USER !== false ? YOURLS_USER : NULL;
if ( amp_have_capability( ampCap::ManageUsrsURL ) // Admin?
|| ( $owner === NULL && amp_have_capability( ampCap::ManageAnonURL ) ) // Editor?
|| ( $owner === $user && amp_have_capability( $capability ) ) ) // Self Edit?
$return = true;
}
return $return;
}
// Check keyword ownership
function amp_keyword_owner( $keyword ) {
global $ydb;
$table = YOURLS_DB_TABLE_URL;
$binds = array( 'keyword' => $keyword );
$sql = "SELECT * FROM `$table` WHERE `keyword` = :keyword";
$result = $ydb->fetchOne($sql, $binds);
return $result['user'];
}
// Record user info on keyword creation
yourls_add_action( 'insert_link', 'amp_insert_link' );
function amp_insert_link($actions) {
global $ydb;
$keyword = $actions[2];
$user = YOURLS_USER !== false ? YOURLS_USER : NULL;
$table = YOURLS_DB_TABLE_URL;
// Insert $keyword against $username
$binds = array( 'user' => $user,
'keyword' => $keyword);
$sql = "UPDATE `$table` SET `user` = :user WHERE `keyword` = :keyword";
$result = $ydb->fetchAffected($sql, $binds);
}
// Quick user validation without triggering hooks
function amp_is_valid_user() {
$valid = defined( 'YOURLS_USER' ) ? true : false;
if ( !$valid ) {
if ( yourls_is_API()
&& isset( $_REQUEST['timestamp'] ) && !empty($_REQUEST['timestamp'] )
&& isset( $_REQUEST['signature'] ) && !empty($_REQUEST['signature'] ) )
$valid = yourls_check_signature_timestamp();
elseif ( yourls_is_API()
&& !isset( $_REQUEST['timestamp'] )
&& isset( $_REQUEST['signature'] ) && !empty( $_REQUEST['signature'] ) )
$valid = yourls_check_signature();
elseif ( isset( $_REQUEST['username'] ) && isset( $_REQUEST['password'] )
&& !empty( $_REQUEST['username'] ) && !empty( $_REQUEST['password'] ) )
$valid = yourls_check_username_password();
elseif ( !yourls_is_API() && isset( $_COOKIE[ yourls_cookie_name() ] ) )
$valid = yourls_check_auth_cookie();
}
return $valid;
}
?>