hide elements based on Wordpress user role - hide

really appreciate more input into my code :)
I try to hide search field based on user role:
add_filter ('bps_before_search_form_filter' , 'hide_field_19_wrap', 10, 1);
$array1 = array(field_19_wrap, field_28_match_any_wrap, field_34_distance_wrap);
function hide_field_19_wrap ($field_ids){
global $current_user;
$user_roles = $current_user->roles;
if (!current_user_can('subscriber')) {
unset($array1[field_19_wrap]);
}
Return $field_ids;
}

Related

Security - The view and edit id is visible in the address bar

CakePHP Version 3.5.5
The id is visible in the address bar for view and edit which for my application creates a security risk. Any logged in user at the same company can change the id in the address bar and view or edit the details
of users they are not allowed to.
IE: https://localhost/crm/users/edit/1378 can be manually changed in the address bar to https://localhost/crm/users/edit/1215 and entered. This would display the details of user 1215 which is not allowed.
To overcome this I am selecting the ids which the user is allowed to edit and checking that the id from the url is one of these ids with the following code:
public function view($id = null)
{
if ($this->request->is('get')) {
// Select the permitted ids.
if (superuser) { // example to explain only
$query = $this->Users->find()
->where(['companyid' => $cid])
->andWhere(['status' => 1])
->toArray();
}
elseif (manager) { // example to explain only
$query = $this->Users->find()
->where(['areaid' => $areaid])
->andWhere(['status' => 1])
->toArray();
}
elseif (team leader) { // example to explain only
$query = $this->Users->find()
->where(['teamid' => $teamid])
->andWhere(['status' => 1])
->toArray();
}
// Check if the edit id is in the array of permitted ids.
$ids = array_column($query, 'id');
$foundKey = array_search($id, $ids);
// If the edit id is not in the array of permitted ids redirect to blank.
if (empty($foundKey)) {
// Handle error.
}
$user = $this->Users->get($id);
$this->set('user', $user);
$this->set('_serialize', ['user']);
}
else {
// Handle error.
}
}
My question: Is the above code the best cake way of achieving this or is there a better way to do it?
This code does work but because it's to do with security I'd appreciate any input which would improve it or point out it's weakness/es.
/////////////////////////////////////////////////////////////////////////////
As requested by cgTag please see below.
My app has superusers, managers, team leaders and users.
Managers manage one area which can contain many teams.
Team Leaders lead one team and must belong to an area.
Users are assigned to an area or a team.
For example:
Area is UK
Team is England
Team is Scotland
Team is Wales
Area is USA
Team is Florida
Team is California
Team is Texas
On index - superusers see all the superusers, managers, team leaders and users in the company.
On index - managers see themself and users in their area, team leaders in their area and users in the teams.
On index - team leaders see themself and users in their team
My problem is say the manager of area UK clicks edit on one of the records and that record is displayed with a url of https://localhost/crm/users/edit/1378
Then say this disgruntled manager makes a guess and changes the url to https://localhost/crm/users/edit/1215 and submits it then this record is displayed. (This record could be anyone, a superuser, another manager, a team leader who is not in their area or a user not in their area.
This manager could then change say the email address and submit this and it's this type of situation that I need to protect against.
My fix is to reiterate the find for the superuser, manager and team leader I've done on index in the view and edit class. This ensures that say a manager can only view or edit someone in their area.
Hopefully I've explained it well enough but if not just let me know and I'll have another go.
Thanks. Z.
/////////////////////////////////////////////////////////////////////////////
Thanks cgTag, I feel a lot more confident with this approach but I cannot use this code because you have correctly assumed that I am using an id to select all the companies results but I'm using a 40 char string. I do this so I can make my sql queries more robust.
It's impossible for you to help me unless you have all the info required so I have posted an accurate representation below:
public function view($id = null)
{
if(!$this->request->is('get') || !$id) {
//throw new ForbiddenException();
echo 'in request is NOT get or id NOT set ' . '<hr />';
}
$user_id = $this->Auth->user('id');
// regular users can never view other users.
if($user_id !== $id) {
//throw new ForbiddenException();
echo 'in $user_id !== $id ' . '<hr />';
}
// Declare client id 1.
if ($this->cid1() === false) {
echo 'in throw exception ' . '<hr />';
}
else {
$c1 = null;
$c1 = $this->cid1();
}
$company_ids = $this->getCompanyIds($c1);
$area_ids = $this->getAreaIds($user_id, $c1);
$team_ids = $this->getTeamIds($user_id, $c1);
// company_id does not exist which will cause an unknown column error.
// The column I select by is cid_1 so I have changed this column to cid_1 as shown below.
$user = $this->Users->find()
->where([
'id' => $id,
'cid_1 IN' => $company_ids,
'area_id IN' => $area_ids,
'team_id IN' => $team_ids,
'status' => 1
])
->firstOrFail();
$this->set(compact('user'));
}
The functions:
public function cid1()
{
$session = $this->request->session();
if ($session->check('Cid.one')) {
$c1 = null;
$c1 = $session->read('Cid.one');
if (!is_string($c1) || is_numeric($c1) || (strlen($c1) !== 40)) {
return false;
}
return $c1;
}
return false;
}
public function getCompanyIds($c1 = null)
{
$query = $this->Users->find()
->where(['status' => 1])
->andWhere(['cid_1' => $c1]);
return $query;
}
public function getAreaIds($c1 = null, $user_id = null)
{
$query = $this->Users->find()
->where(['status' => 1])
->andWhere(['cid_1' => $c1])
->andWhere(['area_id' => $user_id]);
return $query;
}
public function getTeamIds($c1 = null, $user_id = null)
{
$query = $this->Users->find()
->where(['status' => 1])
->andWhere(['cid_1' => $c1])
->andWhere(['team_id' => $user_id]);
return $query;
}
With this code I get the following error:
Error: SQLSTATE[21000]: Cardinality violation: 1241 Operand should contain 1 column(s)
I don't know if your example will work with this new information but at least you have all the information now.
If it can be ammended great but if not I really don't mind. And I do appreciate the time you've put aside to try to help.
Thanks Z
/////////////////////////////////////////////////////////////////////////////
#tarikul05 - Thanks for the input.
Your suggestion is very similar to my first effort at addressing this security issue but I went for security through obscurity and hid the id in a 80 char string, example below.
// In a cell
public function display($id = null)
{
// Encrypt the id to pass with view and edit links.
$idArray = str_split($id);
foreach($idArray as $arrkey => $arrVal) {
$id0 = "$idArray[0]";
$id1 = "$idArray[1]";
$id2 = "$idArray[2]";
$id3 = "$idArray[3]";
}
// Generate string for the id to be obscured in.
$enc1 = null;
$enc1 = sha1(uniqid(mt_rand(), true));
$enc2 = null;
$enc2 = sha1(uniqid(mt_rand(), true));
$encIdStr = $enc1 . $enc2;
// Split the string.
$encIdArray = null;
$encIdArray = str_split($encIdStr);
// Generate the coded sequence.
$codedSequence = null;
$codedSequence = array(9 => "$id0", 23 => "$id1", 54 => "$id2", 76 => "$id3");
// Replace the id in the random string.
$idTemp = null;
$idTemp = array_replace($encIdArray, $codedSequence);
// Implode the array.
$encryptedId = null;
$encryptedId = implode("",$idTemp);
// Send the encrypted id to the view.
$this->set('encryptedId', $encryptedId);
}
And then decrypted with
// In function in the app controller
public function decryptTheId($encryptedId = null)
{
$idArray = str_split($encryptedId);
foreach($idArray as $arrkey => $arrVal) {
$id0 = "$idArray[9]";
$id1 = "$idArray[23]";
$id2 = "$idArray[54]";
$id3 = "$idArray[76]";
}
$id = null;
$id = $id0.$id1.$id2.$id3;
return $id;
}
The problem with this was that when testing I managed to get the script to error which revealed the array positions which would of undermined the security by obscurity principle and made it a lot easier for a hacker.
Your suggestion is neater than my obscurity method but I believe md5 has been cracked therefore it should not be used.
I'm no security expert but in my opinion checking the view and edit id against an array of permitted ids is the most secure way to address this.
Maybe I'm wrong but if I do it this way there's is no way a hacker no matter what they try in the address bar can see or edit data they are not meant to and it keeps the url cleaner.
What I was originally looking/hoping for was a Cake method/function which addressed this but I couldn't find anything in the cookbook.
Thanks anyway. Z.
I would simplify your code so that the SQL that fetches the user record only finds that record if the current user has permissions. When you're dependent upon associated data for those conditions. Follow this approach even if you have to use joins.
You create the SQL conditions and then call firstOrFail() on the query. This throws a NotFoundException if there is no match for the record.
public function view($id = null) {
if(!$this->request->is('get') || !$id) {
throw new ForbiddenException();
}
$user_id = $this->Auth->user('id');
// regular users can never view other users.
if($user_id !== $id) {
throw new ForbiddenException();
}
$company_ids = $this->getCompanyIds($user_id);
$area_ids = $this->getAreaIds($user_id);
$team_ids = $this->getTeamIds($user_id);
$user = $this->Users->find()
->where([
'id' => $id
'company_id IN' => $company_ids,
'area_id IN' => $area_ids,
'team_id IN' => $team_ids,
'status' => 1
])
->firstOrFail();
$this->set(compact('user'));
}
The above logic should be sound when a user belongsTo a hierarchical structure of data. Where by, they can view many users but only if those users belong to one of the upper associations they have access too.
It works because of the IN clause of the where conditions.
Note: The IN operator throws an error if the array is empty. When you have users who can see all "teams" just exclude that where condition instead of using an empty array.
The key here is to have functions which return an array of allowed parent associations such as; getCompanyIds($user_id) would return just the company IDs the current user is allowed access too.
I think if you implement it this way then the logic is easy to understand, the security is solid and a simple firstOrFail() prevents access.

Distinct values in Azure Search Suggestions?

I am offloading my search feature on a relational database to Azure Search. My Products tables contains columns like serialNumber, PartNumber etc.. (there can be multiple serialNumbers with the same partNumber).
I want to create a suggestor that can autocomplete partNumbers. But in my scenario I am getting a lot of duplicates in the suggestions because the partNumber match was found in multiple entries.
How can I solve this problem ?
The Suggest API suggests documents, not queries. If you repeat the partNumber information for each serialNumber in your index and then suggest based on partNumber, you will get a result for each matching document. You can see this more clearly by including the key field in the $select parameter. Azure Search will eliminate duplicates within the same document, but not across documents. You will have to do that on the client side, or build a secondary index of partNumbers just for suggestions.
See this forum thread for a more in-depth discussion.
Also, feel free to vote on this UserVoice item to help us prioritize improvements to Suggestions.
I'm facing this problem myself. My solution does not involve a new index (this will only get messy and cost us money).
My take on this is a while-loop adding 'UserIdentity' (in your case, 'partNumber') to a filter, and re-search until my take/top-limit is met or no more suggestions exists:
public async Task<List<MachineSuggestionDTO>> SuggestMachineUser(string searchText, int take, string[] searchFields)
{
var indexClientMachine = _searchServiceClient.Indexes.GetClient(INDEX_MACHINE);
var suggestions = new List<MachineSuggestionDTO>();
var sp = new SuggestParameters
{
UseFuzzyMatching = true,
Top = 100 // Get maximum result for a chance to reduce search calls.
};
// Add searchfields if set
if (searchFields != null && searchFields.Count() != 0)
{
sp.SearchFields = searchFields;
}
// Loop until you get the desired ammount of suggestions, or if under desired ammount, the maximum.
while (suggestions.Count < take)
{
if (!await DistinctSuggestMachineUser(searchText, take, searchFields, suggestions, indexClientMachine, sp))
{
// If no more suggestions is found, we break the while-loop
break;
}
}
// Since the list might me bigger then the take, we return a narrowed list
return suggestions.Take(take).ToList();
}
private async Task<bool> DistinctSuggestMachineUser(string searchText, int take, string[] searchFields, List<MachineSuggestionDTO> suggestions, ISearchIndexClient indexClientMachine, SuggestParameters sp)
{
var response = await indexClientMachine.Documents.SuggestAsync<MachineSearchDocument>(searchText, SUGGESTION_MACHINE, sp);
if(response.Results.Count > 0){
// Fix filter if search is triggered once more
if (!string.IsNullOrEmpty(sp.Filter))
{
sp.Filter += " and ";
}
foreach (var result in response.Results.DistinctBy(r => new { r.Document.UserIdentity, r.Document.UserName, r.Document.UserCode}).Take(take))
{
var d = result.Document;
suggestions.Add(new MachineSuggestionDTO { Id = d.UserIdentity, Namn = d.UserNamn, Hkod = d.UserHkod, Intnr = d.UserIntnr });
// Add found UserIdentity to filter
sp.Filter += $"UserIdentity ne '{d.UserIdentity}' and ";
}
// Remove end of filter if it is run once more
if (sp.Filter.EndsWith(" and "))
{
sp.Filter = sp.Filter.Substring(0, sp.Filter.LastIndexOf(" and ", StringComparison.Ordinal));
}
}
// Returns false if no more suggestions is found
return response.Results.Count > 0;
}
public async Task<List<string>> SuggestionsAsync(bool highlights, bool fuzzy, string term)
{
SuggestParameters sp = new SuggestParameters()
{
UseFuzzyMatching = fuzzy,
Top = 100
};
if (highlights)
{
sp.HighlightPreTag = "<em>";
sp.HighlightPostTag = "</em>";
}
var suggestResult = await searchConfig.IndexClient.Documents.SuggestAsync(term, "mysuggestion", sp);
// Convert the suggest query results to a list that can be displayed in the client.
return suggestResult.Results.Select(x => x.Text).Distinct().Take(10).ToList();
}
After getting top 100 and using distinct it works for me.
You can use the Autocomplete API for that where does the grouping by default. However, if you need more fields together with the result, like, the partNo plus description it doesn't support it. The partNo will be distinct though.

Check if a given user is in the security group of a given path

I have a simple job which I don't know how to accomplish, and as deeper I search it I got lost deeper.
I need to write a method which returns the FileSystemAccessRule of a given user(I am given his samAccountName, objectGUID) over a given folder path.
I've done adding or removing FileSystemAccessRule's to a path before like this:
var fSecurity = Directory.GetAccessControl(physicalPath);
fSecurity.AddAccessRule(new FileSystemAccessRule(samAccountName, FileSystemRights.FullControl, AccessControlType.Allow));
fSecurity.RemoveAccessRule(new FileSystemAccessRule(samAccountName, FileSystemRights.FullControl, AccessControlType.Allow));
Directory.SetAccessControl(physicalPath, fSecurity);
Checking if the given user has some certain the access rights over a path a similiar job? Or should go to another way? Something like DirectoryEntry or LDAP or Active Directory or so?
What I want is a method which maybe looks like this:
FileSystemAccessRule[] GetAccessRulesOfTheUserOverPath(string samAccountName, string folderPath)
{
/// how?
}
Thanks to some answers on SO I've come up with an answer. Although it is not the exact answer to my question, it fulfills my need. On this question's answers I found the solution. This solution tells me if the given FileSystemRights is bound to the current windows user on acl(AuthorizationRuleCollection) of given folder.
Almost all answers in the question I've referred to are giving the result, In my opinion the most accurate one is #Olivier Jacot-Descombes's answer since it calculates the allow rules, deny rules, and inherited rules precedences over each other.
So what I did is this:
WindowsIdentity _currentUser;
WindowsPrincipal _currentPrincipal;
using ( new Impersonator(userName, passwordOfTheUser) )
{
_currentUser = WindowsIdentity.GetCurrent();
_currentPrincipal = new WindowsPrincipal(WindowsIdentity.GetCurrent());
}
if ( !Directory.Exists(path) ) throw new Exception("Directory does not exist");
var di = new DirectoryInfo(path);
var directoryACLs = di.GetAccessControl().GetAccessRules(true, true, typeof(SecurityIdentifier));
///rw_accessRules list consists of the rules for ReadWrite permissons.
bool auth_RW = rw_accessRules.All(aR => HasFileOrDirectoryAccess(_currentUser, _currentPrincipal, aR, directoryACLs));
And here is the ``HasFileOrDirectoryAccess` method:
bool HasFileOrDirectoryAccess ( WindowsIdentity _currentUser, WindowsPrincipal _currentPrincipal, FileSystemRights right, AuthorizationRuleCollection acl )
{
bool allow = false;
bool inheritedAllow = false;
bool inheritedDeny = false;
foreach ( FileSystemAccessRule currentRule in acl )
{
// If the current rule applies to the current user.
if ( _currentUser.User.Equals(currentRule.IdentityReference) || _currentPrincipal.IsInRole((SecurityIdentifier)currentRule.IdentityReference) )
{
if ( currentRule.AccessControlType.Equals(AccessControlType.Deny) )
{
if ( ( currentRule.FileSystemRights & right ) == right )
{
if ( currentRule.IsInherited )
{
inheritedDeny = true;
}
else
{ // Non inherited "deny" takes overall precedence.
return false;
}
}
}
else if ( currentRule.AccessControlType.Equals(AccessControlType.Allow) )
{
if ( ( currentRule.FileSystemRights & right ) == right )
{
if ( currentRule.IsInherited )
{
inheritedAllow = true;
}
else
{
allow = true;
}
}
}
}
}
if ( allow )
{ // Non inherited "allow" takes precedence over inherited rules.
return true;
}
return inheritedAllow && !inheritedDeny;
}
I first impersonate for the given user, get his principal and identity, then check if he has the authority of the given rule set.
This one works for my case, but you'll notice that we need password of the user that we want check the permissions of. If there is any way to do this without the password, it will be great.

jquery-jable: How to display a field as read-only in the edit form?

I have a table pre-populated with the company LAN IP addresses with fields for associated data, status, etc. The (jquery-)jtable fields collection is configured like this.
fields: {
id: { title: 'ID'},
ip: { title: 'IP address, edit: false }
more: { ... }
}
This works but the problem is that when the edit dialog pops up the user can't see the ip address of the record being edited as jtable's edit form doesn't show the field.
I've read through the documentation but can't see any way to display a field as read-only in the edit form. Any ideas?
You don't need to hack the jTable library asset, this just leads to pains when you want to update to a later version. All you need to do is create a custom input via the jTable field option "input", see an example field setup to accomplish what you need here:
JobId: {
title: 'JobId',
create: true,
edit: true,
list: true,
input: function (data) {
if (data.value) {
return '<input type="text" readonly class="jtable-input-readonly" name="JobId" value="' + data.value + '"/>';
} else {
//nothing to worry about here for your situation, data.value is undefined so the else is for the create/add new record user interaction, create is false for your usage so this else is not needed but shown just so you know when it would be entered
}
},
width: '5%',
visibility: 'hidden'
},
And simple style class:
.jtable-input-readonly{
background-color:lightgray;
}
I have simple solution:
formCreated: function (event, data)
{
if(data.formType=='edit') {
$('#Edit-ip').prop('readonly', true);
$('#Edit-ip').addClass('jtable-input-readonly');
}
},
For dropdown make other options disabled except the current one:
$('#Edit-country option:not(:selected)').attr('disabled', true);
And simple style class:
.jtable-input-readonly{
background-color:lightgray;
}
I had to hack jtable.js. Start around line 2427. Changed lines are marked with '*'.
//Do not create element for non-editable fields
if (field.edit == false) {
//Label hack part 1: Unless 'hidden' we want to show fields even though they can't be edited. Disable the 'continue'.
* //continue;
}
//Hidden field
if (field.type == 'hidden') {
$editForm.append(self._createInputForHidden(fieldName, fieldValue));
continue;
}
//Create a container div for this input field and add to form
var $fieldContainer = $('<div class="jtable-input-field-container"></div>').appendTo($editForm);
//Create a label for input
$fieldContainer.append(self._createInputLabelForRecordField(fieldName));
//Label hack part 2: Create a label containing the field value.
* if (field.edit == false) {
* $fieldContainer.append(self._myCreateLabelWithText(fieldValue));
* continue; //Label hack: Unless 'hidden' we want to show fields even though they can't be edited.
* }
//Create input element with it's current value
After _createInputLabelForRecordField add in this function (around line 1430):
/* Hack part 3: Creates label containing non-editable field value.
*************************************************************************/
_myCreateLabelWithText: function (txt) {
return $('<div />')
.addClass('jtable-input-label')
.html(txt);
},
With the Metro theme both the field name and value will be grey colour.
Be careful with your update script that you're passing back to. No value will be passed back for the //edit: false// fields so don't include them in your update query.
A more simple version for dropdowns
$('#Edit-country').prop('disabled',true);
No need to disable all the options :)

Drupal: using query string data in Views

i have several moderator roles in my drupal site. the users with this roles can create content of specific content-type called News. let's call the roles the following: role_a, role_b, role_c, ...
now i have a View that shows the last 5 News elements.
my question is how to granulate the News elements in View according to the query string?
i mean on page http://mysite.com/a i want to see only the news that was added by the user with the "a" role. http://mysite.com/b is for the "b"-roled user. etc.
how can i use the query string parameters in the Views filter?
I think you mean you want to use an Argument, rather than the query string. In any case, I don't think Views can handle rolenames by default (it can handle role IDs just fine), so you'll have to modify your view query in order to achieve what you want.
First, add User: Roles as an argument in your View. Then, in a custom module, implement hook_views_query_alter() and modify the query by replacing the rolename with its role ID.
function MYMODULE_views_query_alter(&$view, &$query) {
if ($view->name == 'my_view') {
$rolename = '';
foreach ($query->where as $where_index => $where) {
// find the role ID clause
$clause_index = array_search('users_roles.rid = %d', $where['clauses']);
if ($clause_index !== FALSE) {
// found it, so get the rolename
$rolename = $where['args'][$clause_index];
break;
}
}
// if the rolename argument was found
if (!empty($rolename)) {
// get the role ID
$user_roles = user_roles();
$rid = array_search($rolename, $user_roles);
// if the role exists, then replace the argument
if ($rid !== FALSE) {
$query->where[$where_index]['args'][$clause_index] = $rid;
}
}
}
}
So, for example, if your url is http://mysite.com/a, then it will look up the ID of role 'a', then find all nodes by an author with that role. It will also take the actual role ID - for example, if the ID of role 'a' is 10, then http://mysite.com/10 will also return the same result.
If you want it only to look up rolenames, you can modify the hook to fail when it doesn't find the role (just make $rid = 0 and you shouldn't get any results).
function MYMODULE_views_query_alter(&$view, &$query) {
if ($view->name == 'my_view') {
$rolename = '';
foreach ($query->where as $where_index => $where) {
// find the role ID clause
$clause_index = array_search('users_roles.rid = %d', $where['clauses']);
if ($clause_index !== FALSE) {
// found it, so get the rolename
$rolename = $where['args'][$clause_index];
break;
}
}
// if the rolename argument was found
if (!empty($rolename)) {`enter code here`
// get the role ID
$user_roles = user_roles();
$rid = array_search($rolename, $user_roles);
// if the role exists, then replace the argument
if ($rid !== FALSE) {
$query->where[$where_index]['args'][$clause_index] = $rid;
}
}
}
}

Resources