Preventing Infinite Redirects in Access Control Library - php

I am working on an Open Source Role Based Access Control Library for PHP called PHP-Bouncer. PHP-Bouncer allows the user to define a list of roles, which pages each role provides access to, and each role may also define a list of pages which override other pages (so going to the overridden page will redirect you to the overriding page). Here is an example of how this would work (From the Access Managed Example in the documentation):
$bouncer = new Bouncer();
// Add a role Name, Array of pages role provides
$bouncer->addRole("Public", array("index.php", "about.php", "fail.php"));
// Add a role Name, Array of pages role provides
$bouncer->addRole("Registered User", array("myaccount.php", "editaccount.php", "viewusers.php"));
// Add a role Name, Array of pages role provides List of pages that are overridden by other pages
$bouncer->addRole("Admin", array("stats.php", "manageusers.php"), array("viewusers.php" => "manageusers.php"));
// Here we add some users. The user class here extends the BouncerUser class, so it can still do whatever you
// would normally create a user class to do..
$publicUser = new User();
$registeredUser = new User();
$adminUser = new User();
$registeredAndAdmin = new User();
$publicUser->addRole("Public");
$registeredUser->addRole("Public"); // We add the public group to all users since they need it to see index.php
$registeredUser->addRole("Registered User");
$adminUser->addRole("Public"); // We add the public group to all users since they need it to see index.php
$adminUser->addRole("Admin");
$registeredAndAdmin->addRole("Public"); // We add the public group to all users since they need it to see index.php
$registeredAndAdmin->addRole("Registered User");
$registeredAndAdmin->addRole("Admin");
$bouncer->manageAccess($publicUser->getRoles(), substr($_SERVER["PHP_SELF"], 1), "fail.php");
Here's the problem I am having: In the manageAccess function you see above, everything works well as long as the roles are defined sanely, and all users have access to fail.php (or fail.php does not implement the $bouncer object). As soon as someone creates a role which has an inherent conflict (such as overriding a page with itself) or fails to give all users access to the failure page, the manageAccess function results in an infinite loop. Since this is bad, I would like to fix it. I am not sure however, what would be the best approach for allowing a few redirects (it is feasible that redirecting up to two or three times could be desired behaviour) while preventing an infinite loop. Here is the manageAccess function:
/**
* #param array $roleList
* #param string $url
* #param string $failPage
*/
public function manageAccess($roleList, $url, $failPage = "index.php"){
$granted = false;
foreach($roleList as $role){
if(array_key_exists($role, $this->roles)){
$obj = $this->roles[$role];
/** #var $obj BouncerRole */
$response = $obj->verifyAccess($url);
if($response->getIsOverridden()){ // If access to the page is overridden forward the user to the overriding page
$loc = ($obj->getOverridingPage($url) !== false) ? $obj->getOverridingPage($url) : $failPage;
$locationString = "Location: ".$loc;
header($locationString);
// I broke something in the last commit, perhaps this comment will help?
}
if($response->getIsAccessible()){ // If this particular role contains access to the page set granted to true
$granted = true; // We don't return yet in case another role overrides.
}
}
}
// If we are here, we know that the page has not been overridden
// so let's check to see if access has been granted by any of our roles.
// If not, the user doesn't have access so we'll forward them on to the failure page.
if(!$granted){
$locationString = "Location: ".$failPage."?url=".urlencode($url)."&roles=".urlencode(serialize($roleList));
header($locationString);
}
}
Any Suggestions?

Since your desired functionality is to allow "a few redirects", the best way I can think of approaching this is creating either a $_SESSION (or $_GET I suppose would work) parameter that you increment every time you issue a redirect. I should point out that from a user standpoint this would be pretty annoying to deal with, but it is your website.
Let's assume we named the $_SESSION parameter num_redirects:
Before we redirect, check to see if $_SESSION['num_redirects'] is set. If it is not, set it to 0.
Get the current value of $_SESSION['num_redirects'], and see if it is above the redirect threshold.
If it is above the threshold, do not redirect, simply die();
If it is not above the threshold, increment $_SESSION['num_redirects'], store it back, and redirect.
So, that code would look like this (I've also improved your URL query string with a call to http_build_query():
Somewhere, we need to define the threshold:
define( 'MAX_NUM_REDIRECTS', 3);
Then, we can do:
// Assuming session_start(); has already occurred
if(!$granted) {
if( !isset( $_SESSION['num_redirects'])) {
$_SESSION['num_redirects'] = 0;
}
// Check if too many redirects have occurred
if( $_SESSION['num_redirects'] >= MAX_NUM_REDIRECTS) {
die( "Severe Error: Misconfigured roles - Maximum number of redirects reached\n");
}
// If we get here, we can redirect the user, just add 1 to the redirect count
$_SESSION['num_redirects'] += 1;
$query_string = http_build_query( array(
'url' => $url,
'roles' => serialize($roleList)
));
$locationString = "Location: " . $failPage . '?' . $query_string;
header($locationString);
exit(); // Probably also want to kill the script here
}
That's it! Note that you'll have to add the same logic for the first call to header(). If you're going to repeat this functionality elsewhere, it might be worthwhile to encapsulate redirection within a helper function:
function redirect( $url, array $params = array()) {
if( !isset( $_SESSION['num_redirects'])) {
$_SESSION['num_redirects'] = 0;
}
// Check if too many redirects have occurred
if( $_SESSION['num_redirects'] >= MAX_NUM_REDIRECTS) {
die( "Severe Error: Maximum number of redirects reached\n");
}
// If we get here, we can redirect the user, just add 1 to the redirect count
$_SESSION['num_redirects'] += 1;
$query_string = http_build_query( $params);
$locationString = "Location: " . $url . ( !empty( $query_string) ? ('?' . $query_string) : '');
header($locationString);
exit(); // Probably also want to kill the script here
}
Then, you can call it like this:
if(!$granted){
redirect( $failPage, array(
'url' => $url,
'roles' => serialize($roleList)
));
}
This way you can encapsulate all of the logic necessary to redirecting within that one function.

Related

using wp_authenticate() to redirect certain users when logging in

Our website is using Wordpress - WooCommerce login page for customers to login.
I am trying to use wp_authenticate() to achieve the following:
1) Customer login to our new website, enter their username and password, and hit Login button. In case you want to see WooCommerce Login file, click here.
2) Our new website goes through list see if the username matches. If the username matches, don't even look at the password, just redirect the user to other url such as google.com
3) if the username doesn't match with our list, just let them login as usual.
With JQuery, someone helped me to come up with the following code:
var names = new Array(”BILL”, ”JIM”, ”BOB”); // get all names into array, and all in uppercase
var dest_url = ”http://www.website.com”; // URL we want to send them to
jQuery(document).ready(function () {
jQuery(”input[name=’login’]”).click(function(event){
event.preventDefault(); // prevent default form action
var current_name = jQuery(”#username”).val();
current_name = current_name.trim().toUpperCase();
if ( -1 != jQuery.inArray(current_name, names) ) {
alert(”Redirecting ” + current_name + ” to ” + dest_url);
window.location = dest_url; // send to desired URL
}
else
document.getElementsByClassName(”login”)[0].submit(); // input name not on our list, so just do normal submit action
});
});
But I am not sure if wp_authenticate() can actually contain jquery script inside. Any suggestion would be greatly appreciated.
First, I would recommend doing this in PHP, not javascript.
Second, you have a couple of options, leveraging the built-in functionality of WordPress.
If all you care about is the username, and do not care if they successfully logged in with the right password, then you could leverage the filter found in wp_authenticate()
// This is the filter wp_authenticate fires
apply_filters( 'authenticate', null, $username, $password );
Knowing that, you could write a quick little plugin, or add this code to your theme's functions.php file:
// Run this filter with priority 9999 (last, or close to last), after core WP filters have run
add_filter('authenticate', 'redirect_certain_users', 9999, 3);
// Your custom filter function
function redirect_certain_users( $user, $username, $password) {
// Assumes you write a function called get_list_of_users_to_redirect that returns an array similar to that in your sample code
$redirect_users = get_list_of_users_to_redirect();
// If the user is in the array of users to redirect, then send them away
if ( in_array( $username, $redirect_users ) ) {
header("location:http://www.example.com");
die();
}
return $user;
}
Note that this code is untested, but should get you at least 90% of the way there.

Activate a Topic for a Page programmaticaly concrete5 5.7

The goal is to activate an existent topic in a Blog Entry page. Normally a user does this in the Pages Attributes section like so:
Now my goal is to do this programmaticaly. I won't post all my trials (since 2 days) here because it's just crap, but here's what I've done so far.
First I add a Blog Page to a chosen parent Page (ID 157):
use Concrete\Core\Page;
$parentPage = Page\Page::getByID(157);
$template = \PageTemplate::getByHandle('blog_entry');
$entry = $parentPage->add($type, array(
'cName' => 'My title',
'cDescription' => 'description',
'cHandle' => 'my_title',
'cvIsApproved' => true,
'cDatePublic' => $publishDate->format('Y-m-d H:i:s')
), $template);
As the newly created page is a blog_entry template the Blog Entry Topics is already assigned.
Then I create a Topic and add it to its Topic Tree (Blog Entry Topics) like so:
use \Concrete\Core\Tree\Type\Topic as TopicTree;
use \Concrete\Core\Tree\Node\Type\Topic as TopicTreeNode;
use \Concrete\Core\Tree\Node\Node as TreeNode;
$topicTree = TopicTree::getByName('Blog Entries');
$parentTopic = TreeNode::getByID($topicTree->getRootTreeNodeObject()->treeNodeID);
$item0 = TopicTreeNode::add('udland', $parentTopic);
How to activate/assign this Topic(Udland) to my page ($entry)? (As shown in the image)
I know it must be related to the DB-tables CollectionAttributeValues and atSelectedTopics. Also the Classes CollectionValue and CollectionKey must be involved.
I could add those entries manually in the DB but this isn't a good idea because I don't know what data is necessary to make this work correctly. The topics are used to filter Blog entries so I'm quite sure that there are other tables involved and as a Core developer said: "These are fragile little things" ;-).
As this version of concrete5 is a complete new launch, the developer docs aren't complete and after 2 days of digging inside the core code I'm just desperate.
Update (after a week of digging...)
I managed to do a hack taken out of a Controller method: (/concrete/controllers/panel/page/attributes.php -> submit()).
I know this isn't the way to go at all but it's my best trial so far:
(I just include the NameSpaces here to make clear what Classes I'm calling)
use Concrete\Core\Page;
use Concrete\Core\Page\Collection\Version\Version;
use Concrete\Core\Workflow\Request\ApprovePageRequest;
use CollectionAttributeKey;
use \Concrete\Core\Tree\Node\Type\Topic as TopicTreeNode;
Get the Attributes ID by handle:
$ak = CollectionAttributeKey::getByHandle('blog_entry_topics');
$attributekID = $ak->getAttributeKeyID();
get the topic
$item_one = TopicTreeNode::getNodeByName('Udland');
then simulate a posted form by:
$_POST = array(
'topics_' . $attributekID => array($item_one->treeNodeID)
);
I know this is so ugly and a big hack & not reliable at all but as said it's taken out of a Controller...
Then I do a slimmed version of the submit() method:
$c = Page\Page::getByID(157);
$published = new \DateTime();
$nvc = $c->getVersionToModify();
$nvcObj = $nvc->getVersionObject();
$data = array();
$data['cName'] = $nvcObj->cvName;
$data['cDescription'] = $nvcObj->cvDescription;
$data['cDatePublic'] = $published->format('Y-m-d H:i:s');
$data['uID'] = '1';
$nvc->update($data);
$setAttribs = $nvc->getSetCollectionAttributes();
$processedAttributes = array();
$selectedAKIDs = $attributekID;
if (!is_array($selectedAKIDs)) {
$selectedAKIDs = array();
}
$selected = is_array(array($attributekID)) ? array($attributekID) : array();
foreach ($setAttribs as $ak) {
if (in_array($ak->getAttributeKeyID(), $selected)) {
$ak->saveAttributeForm($nvc);
} else {
$nvc->clearAttribute($ak);
}
$processedAttributes[] = $ak->getAttributeKeyID();
}
$newAttributes = array_diff($selectedAKIDs, $processedAttributes);
foreach ($newAttributes as $akID) {
$ak = CollectionAttributeKey::getByID($akID);
$ak->saveAttributeForm($nvc);
}
So as said before this is really ugly but it's the best trial so far and somehow it works.
Then approve the Request by doing:
$pkr = new ApprovePageRequest();
$u = new User();
$pkr->setRequestedPage($c);
$v = Version::get($c, "RECENT");
$pkr->setRequestedVersionID($v->getVersionID());
$pkr->setRequesterUserID($u->getUserID());
$pkr->trigger();
$u->unloadCollectionEdit();
But what really makes me wonder is that method inside of /concrete/src/Attribute/Key/Key.php where finally the thing should happen (in my humble opinion):
/**
* Calls the functions necessary to save this attribute to the database. If no passed value is passed, then we save it via the stock form.
* NOTE: this code is screwy because all code ever written that EXTENDS this code creates an attribute value object and passes it in, like
* this code implies. But if you call this code directly it passes the object that you're messing with (Page, User, etc...) in as the $attributeValue
* object, which is obviously not right. So we're going to do a little procedural if/then checks in this to ensure we're passing the right
* stuff
*
* #param CollectionValue|mixed $mixed
* #param mixed $passedValue
*/
protected function saveAttribute($mixed, $passedValue = false)
{
/** #var \Concrete\Core\Attribute\Type $at */
$at = $this->getAttributeType();
$at->getController()->setAttributeKey($this);
if ($mixed instanceof AttributeValue) {
$attributeValue = $mixed;
} else {
// $mixed is ACTUALLY the object that we're setting the attribute against
//todo: figure out what $nvc should really be since it doesn't exist in this scope
$attributeValue = $nvc->getAttributeValueObject($mixed, true);
}
$at->getController()->setAttributeValue($attributeValue);
if ($passedValue) {
$at->getController()->saveValue($passedValue);
} else {
$at->getController()->saveForm($at->getController()->post());
}
$at->__destruct();
unset($at);
}
So I'm really curios to see what the reliable and system-suitable way is to resolve this.
Here's what I came up with that does work. You were pretty close.
use \Concrete\Core\Tree\Type\Topic as TopicTree;
use \Concrete\Core\Tree\Node\Type\Topic as TopicTreeNode;
use \Concrete\Core\Tree\Node\Node as TreeNode;
$parentPage = \Page::getbyPath('/blog');
$template = \PageTemplate::getByHandle('blog_entry');
$entry = $parentPage->add($type, array(
'cName' => 'ooops',
'cDescription' => 'hmmmm',
'cHandle' => 'yay',
'cvIsApproved' => true,
'cDatePublic' => '2015-12-21 00:00:00'
), $template);
$item0 = TopicTreeNode::getNodeByName('udland');
if (!$item0) {
$topicTree = TopicTree::getByName('Blog Entries');
$parentTopic = TreeNode::getByID($topicTree->getRootTreeNodeObject()->treeNodeID);
$item0 = TopicTreeNode::add('udland', $parentTopic);
}
$entry->setAttribute('blog_entry_topics', array($item0->getTreeNodeDisplayPath()));
It looks like the attribute takes in an array of node display paths and that is how it sets the selection. Additionally, you have to use the \Page alias, and not the fully qualified namespace as you were doing, otherwise you get an error about it being unable to clear the cache.

setting persistent plugin parameters in Joomla 3

I'm developing a Joomla 3.x plugin, and want to be able to change the plugin parameter set in the plugin's manifest file programmatically. I believe I need to use a JRegistry object, but I'm not sure about the syntax.
Here's the issue:
// token A is set in plugin params as defined in plugin's XML manifest
var_dump($this->params->get('token')); // prints token "A" as expected
// do some stuff to get a fresh access token, called token "B"
$tokenB = $function_to_get_fresh_token();
// set the new token
if ($tokenB) $this->params->set('token', $tokenB);
var_dump($this->params->get('apptoken')); // prints token "B" as expected
the problem is that on subsequent page reloads, the token reverts to tokenA rather than what I assumed would be the stored value of tokenB.
How do I store the tokenB value in the plugin's parameters in the database?
This is a working example of how to change plugin params from within the plugin (J! 3.4):
// Load plugin called 'plugin_name'
$table = new JTableExtension(JFactory::getDbo());
$table->load(array('element' => 'plugin_name'));
// Params can be changed like this
$this->params->set('new_param', 'new value'); // if you are doing change from a plugin
$table->set('params', $this->params->toString());
// Save the change
$table->store();
Note: If new params are added by plugin dynamically and the plugin is saved afterwards, these new params gets deleted. So one way to deal with it is to add those params as hidden fields to plugin's config XML.
This is just an outline, but something along these lines
$extensionTable = new JtableExtension();
$pluginId = $extensionTable->find('element', 'my_plugin');
$pluginRow = $extensionTable->load($pluginId);
// Do the jregistry work that is needed
// do some stuff to get a fresh access token, called token "B"
$tokenB = $function_to_get_fresh_token();
// set the new token
if ($tokenB) $this->params->set('token', $tokenB);
// more stuff
$extensionTable->save($pluginRow);
I spent a lot of time googling and reading and found no real answer to this. Oddly enough this doesn't seem to have been provided for in Joomla. So here's what I ended up doing:
1) build a function to get your plugin ID, since it will change from one installation to another
private function getPlgId(){
// stupid hack since there doesn't seem to be another way to get plugin id
$db = JFactory::getDBO();
$sql = 'SELECT `extension_id` FROM `#__extensions` WHERE `element` = "my_plugin" AND `folder` = "my_plugin_folder"'; // check the #__extensions table if you don't know your element / folder
$db->setQuery($sql);
if( !($plg = $db->loadObject()) ){
return false;
} else {
return (int) $plg->extension_id;
}
}
2) use the plugin id to load the table object:
$extension = new JTableExtension($db);
$ext_id = $this->getPlgId();
// get the existing extension data
$extension->load($ext_id);
3) when you're ready to store the value, add it to the params, then store it:
$this->params->set('myvalue', $newvalue);
$extension->bind( array('params' => $this->params->toString()) );
// check and store
if (!$extension->check()) {
$this->setError($extension->getError());
return false;
}
if (!$extension->store()) {
$this->setError($extension->getError());
return false;
}
If anyone knows a better way to do this please let me know!

Yiiframework First time login

I'm currently busy with a project that needs users to go to a specific page to create a profile when they log in for the first time (and haven't created one yet). Honestly, I don't know where to start. I would like to do it in a good way.
So in short:
User signs up -> logs in -> needs to fill in form before anything else is allowed -> continue to rest of application
Question: What is a neat way to do this? A solution that isn't going to give me problems in the future development of the application.
I suggest you to use filters. In every controller where the completed profile is neeeded add this code:
public function filters() {
return array(
'completedProfile + method1, method2, method3', // Replace your actions here
);
}
In your base controller (if you don't use base controller, in any controllers) you need to create the filter named completedProfile with the simular code:
public function filterCompletedProfile($filterChain) {
$criteria = new CDBCriteria(array(
'condition' => 'id = :id AND firstname IS NOT NULL AND lastname IS NOT NULL',
'params' => array(':id' => Yii::app()->user->getId())
));
$count = User::model()->count($criteria);
if ($count == 1) {
$filterChain->run();
} else {
$this->redirect(array('user/profile'));
}
}
Possibly add a field to the user profile database table which denotes if they have filled out their profile information. Something like profile_complete. Then you can do a test on pages to see if profile_complete is true and display the page if so, and display the profile page if not.

passing parameters securely in Zend

I am in service controller, login action (/service/login) and I want to pass the name,email and company to profile controller, register action(/profile/register).
Currently I am doing this way
$this->_redirect('/profile/register/?name='.$name.'&emailid='.$email.'&companyid='.$company);
But I want to pass this info to /profile/register in a way such that the user can't see these in the url.
I tried with $this->_forward like this
$params = array(
name=>$name,
emailid=>$email,
companyid=>$company
);
$this->_forward('register','profile',null,$params);
But this isn't working. Is there any other way I can do this?
To pass parameters securely, you should store the data in a session, or if you cannot use sessions, then in the database.
You could use Zend_Session to store the data temporarily until the next page view where you can retrieve it and it will be deleted (or you can let it persist).
$s = new Zend_Session_Namespace('registrationData');
$s->setExpirationHops(1); // expire the namespace after 1 page view
$s->name = $name;
$s->email = $email;
$s->companyId = $company;
// ...
return $this->_redirect('/profile/register);
And then in profile/register:
$s = new Zend_Session_Namespace('registrationData');
$name = $s->name;
$email = $s->email;
$companyId = $s->companyId;
// when this request terminates, the data will be deleted if you leave
// setExpirationHops as 1.
See also Zend_Session - namespace expiration

Categories