Note that some aspects of access control have been changed in Drupal 6.2. This handbook pages reflects these API changes (more).

The access callback and the access arguments decide whether the user has access to a given menu entry or not.

  $items['admin/user/roles'] = array(
    'title' => 'Roles',
    'description' => 'List, edit, or add user roles.',
    'access callback' => 'user_access',
    'access arguments' => array('administer access control'),
  );

The menu system will call the function user_access with the arguments administer access control.

If your module defines access arguments but does not define an access callback, then the menu system defaults to using user_access as the access callback. In other words:

  $items['admin/user/roles'] = array(
    'title' => 'Roles',
    'description' => 'List, edit, or add user roles.',
    'access arguments' => array('administer access control'),
  );

is equivalent to:

  $items['admin/user/roles'] = array(
    'title' => 'Roles',
    'description' => 'List, edit, or add user roles.',
    'access callback' => 'user_access', // menu system adds this callback automatically
    'access arguments' => array('administer access control'),
  );

From Drupal 6.2 on, access callback and access arguments are no longer inherited from parent items. This API change has taken place to ensure every single menu item is correctly secured by access-control. This means to you that:

  • If you define access arguments, then you can be completely positive that user_access will be the default access callback.
  • If you want to use a custom access callback instead, you need to define it for every single menu item.

    Note: Of course you only need to define it for those menu items which you want to use it. The statement is emphasising that the callback is not inherited. If you specify a callback for some menu items but not others, the others will always use user_access.

  • You need to define access arguments for every single menu item.

    Note: If you don't define access arguments then they default to the empty array. If you have defined a custom access callback that takes no arguments (like the printer_friendly_access example above), then this shouldn't pose a problem (so this is the one situation where it actually makes sense *not* to define access arguments). Again, the statement above is emphasising that access arguments are not inherited in any way.

  • Default local tasks being the exception to all of this of course.

The only exception to this is for items of type MENU_DEFAULT_LOCAL_TASK: They still inherit both access callback and access arguments from their parent item.

  $items['user'] = array(
    'title' => 'My account',
    'page callback' => 'user_view',
    'page arguments' => array(1),
    'access callback' => 'user_view_access',
    'access arguments' => array(1),
  );
  $items['user/%user'] = array(
    'title' => 'View',
    'access callback' => 'user_view_access',
    'access arguments' => array(1),
  );
  $items['user/%user/view'] = array(
    'title' => 'View',
    'type' => MENU_DEFAULT_LOCAL_TASK,
  );
  $items['user/%user/edit'] = array(
    'title' => 'Edit',
    'page callback' => 'user_edit',
    'access arguments' => array('administer users'),
    'type' => MENU_LOCAL_TASK,
  );
  $items['user/%user/delete'] = array(
    'title' => 'Delete',
    'page callback' => 'user_edit',
    'access arguments' => array('administer users'),
    'type' => MENU_CALLBACK,
  );
  • 'user/%user' doesn't inherit access callback or access arguments from its parent item. As the callback always defaults to user_access, we generally don't need to define it explicitly. But as we want to use a custom callback instead, we need to define both access callback and access arguments explicitly.
  • 'user/%user/view' inherits both access callback and access arguments from its parent item, as it is an item of the type MENU_DEFAULT_LOCAL_TASK.
  • 'user/%user/edit' and 'user/%user/delete' don't inherit access callback or access arguments from their parent item. As the callback always defaults to user_access, it doesn't need to be explicitly defined. What we need to define explicitly is access arguments.

We support 'access callback' => TRUE (meaning all users and anonymous are allowed access) (and 'access callback' => FALSE for no access by any user).

Often you will find no callback that suits your needs. In this case, you should write one:

function printer_friendly_access() {
  return user_access('access content') && user_access('see printer-friendly version');
}

Of course, the callback can be much more complex:

function user_view_access($account) {
  return $account && $account->uid &&
    (
      // Always let users view their own profile.
      ($GLOBALS['user']->uid == $account->uid) ||
      // Administrators can view all accounts.
      user_access('administer users') ||
      // The user is not blocked and logged in at least once.
      ($account->access && $account->status && user_access('access user profiles'))
    );
}
...
  $items['user/%user_current'] = array(
    'access callback' => 'user_view_access',
    'access arguments' => array(1),
  );

Important note: the value for the 'access callback' must always be a string which is the the name of the function - never a function call. It may also be assigned the value TRUE or FALSE if the page is always (or never) accessible.

Comments

mvc’s picture

Note that using an argument which is a number results in the appropriate path component getting passed to the access, title, or page callback function (i.e. if the argument is array(2) the function will be called with the value of arg(2) as its argument). If you want to pass an actual number, you'll need to hide it in a string.

For example, this will not work, because $og_nid is an integer:

  foreach (og_all_groups_options() as $og_nid => $og_name) {
    $items['my_maps/'. $og_nid] = array(
        'type' => MENU_NORMAL_ITEM,
        'title' => '!group',
        'title arguments' => array('!group' => $og_name),
        'page callback' => 'user_menu_do',
        'page arguments' => array($og_nid, 'view'),
        'access callback' => 'user_menu_access_custom',
        'access arguments' => array('group_member', $og_nid),
        );
  }

Instead, you would have to pass a string:

  foreach (og_all_groups_options() as $og_nid => $og_name) {
    $items['my_maps/'. $og_nid] = array(
        'type' => MENU_NORMAL_ITEM,
        'title' => '!group',
        'title arguments' => array('!group' => $og_name),
        'page callback' => 'user_menu_do',
        'page arguments' => array("nid$og_nid", 'view'),
        'access callback' => 'user_menu_access_custom',
        'access arguments' => array('group_member', "nid$og_nid"),
        );
  }

The access and page callbacks then would need to strip off the text which was added before the nid: $og_nid = substr($og_nid,3)

nyl_auster’s picture

edit : i remove my comment, i understand now how this new writing avoid me to write boring lines of code before an menu item...

mikenewski’s picture

I found your post very helpful, but I ran across a problem when I'm trying to return a value of true. Here's a case where I tried to limit the access of user/1 location to only the admin account.

function my_menu_alter(&$items) {
         $items['user/1'] = array('access arguments' => array(1), 'access callback' => 'check_access');
}

//fatal error
function check_access($account) {
		return TRUE;
}

//works fine
function check_access($account) {
		return FLASE;
}

Returning FALSE works fine. When the check_access returns TRUE i get this error:

Fatal error: require_once() [function.require]: Failed opening required '/user.pages.inc' (include_path='.:/home/httpd/inc:/home/httpd/inc/smarty:/usr/share/pear') in /home/devel/includes/menu.inc on line 346
Page execution time was 236.84 ms.

I'm lost on this, any insight would be great.

duckzland’s picture

try including the user.pages.inc in the check access / module init function

--------------------------------------------------------------------------------------------------------
if you can use drupal why use others?
VicTheme.com

xtfer’s picture

For anyone coming across this in future, its a problem with menu items trying to get files from the locations where they are defined, not actually related to this TRUE/FALSE issue mentioned.

The solution is to add the following 'file path' key to the array, either in hook_menu or hook_menu_alter:

$items['user/1']['file path'] = 'modules/user';
mikenewski’s picture

I've given user administration to a role on my site, and i found that they can view and edit user 1 information, so I tried to set user access using hook_menu_alter so that only user 1 can access user/1:

function my_menu_alter(&$items) {
	$items['user/1']['user_access'] = '_deny_user_access';
}

function _deny_user_access() {
	global $user;
	$uid = $user->uid;
	if($uid == '1')
	{
		return TRUE;
	}
	return FALSE;
}

but when i run this I get access denied even when signed in as user 1. Any one have any ideas on why this is happening?

admin (user)
Access denied
You are not authorized to access this page.
You'll note that I use 'user_access' rather than 'access callback' because i get an access denied rather than a blank page/
Thanks.

DarkMukke’s picture

I had this problem too

your access call back has to be in the .module file and not in the registry or pages include or the hook won't find your custom access callback and always give false
this is (afaik) because your include pages are not exposed to hook_menu or hook_menu_alter
a fix for this might be to define your include files in the .info file

i hope this fixes your problem

edit: Oh and make sure you flushed the menu cache

mikenewski’s picture

Final Solution: I found using the following worked, it's all about getting the right arguments in the menu items (took forever to figure this out). pay close attention to %user_uid_optional, and %user_category. If you just use %user in these places, the page becomes a white screen of death.


function mymodule_menu_alter(&$items) {
	$items['user/%user_uid_optional']['access callback'] = 'mymodule_user_access';
	$items['user/%user_category/edit']['access callback'] = 'mymodule_user_access';
	$items['user/%user/contact']['access callback'] = 'mymodule_user_access';
}


// restrict admin (uid == 1)  view/edit/contact access to admin only
function mymodule_user_access($account) {
	global $user;
	if($user->uid == 1){
		return TRUE;
	}
	if($user->uid == 0){
		return FALSE;
	}
	if($account->uid == 1){
		return FALSE;
	}
	// allow access to owner
	if($user->uid == $account->uid){
		return TRUE;
	}
	return FALSE;
}

For those that don't want to deal with this, I found a module that does most of this for you:
http://drupal.org/project/administerusersbyrole

Thanks to all who tried to help.