I have downloaded the examples project, but in the menu_example module all the access callback are set to true.. hard to understand how it works.

In my example, my meno entry should be visible on nodes, but only for roles that have the permissions to edit his own nodes.

I cant find an example a bit more verbose of an access callback.

Do anyone have one?

2 Answers 2


Edit: I missed the part about the "edit own node" permission, because then you must not only check the permission but also if that node belongs to the current user. I've updated my example below but I'm leaving the above explanation as it was.

Is your menu entry below node/nid (e.g. node/1234/something)? Then you probably don't even need a custom access callback.

If you define your menu path like the following example, it will only call the access callback (and therefore your page callback), if you're viewing a valid node.


This means that it will call node_load(1234) for the above example and only continue if a valid node object is returned. So you can define your permission with access arguments as usual.

That said, writing an access callback is really simple. It's just a function that will receive whatever arguments you defined in access arguments. For example, the default access callback is user_access() and when you define your access arguments like 'access arguments' => array('a permission string'), it will result in the following call: user_access('a permission string').

If you have multiple arguments, these will be passed as second, third and so on argument to your function. To access the currently active node, you can use menu_get_object().

So you could write your access callback like this, but again, you might not even need to create one.

function yourmodule_access_check() {
  global $user;
  $node = menu_get_object();

  return $node && $node->uid == $user->uid && user_access('edit own ' . $node->type . ' content');

Instead of hardcoding the permission string, you could pass it as an argument to the function or whatever you want to do.

  • could never achieve last example : with $items['node/%node/edit']['access callback'] = 'admin_access_only'; and $node = menu_get_object(); in the callback fn, $node never returned anything. I used instead $node = node_load(arg(1)); which worked... Further explanations would be really welcome
    – Kojo
    Jul 24, 2015 at 20:37

Drupal is itself an example of how to write code.

The easier example is aggregator_menu(), which contains the following code.

  $items['admin/config/services/aggregator'] = array(
    'title' => 'Feed aggregator', 
    'description' => "Configure which content your site aggregates from other sites, how often it polls them, and how they're categorized.", 
    'page callback' => 'aggregator_admin_overview', 
    'access arguments' => array('administer news feeds'), 
    'weight' => 10, 
    'file' => 'aggregator.admin.inc',
  $items['admin/config/services/aggregator/add/feed'] = array(
    'title' => 'Add feed', 
    'page callback' => 'drupal_get_form', 
    'page arguments' => array('aggregator_form_feed'), 
    'access arguments' => array('administer news feeds'), 
    'type' => MENU_LOCAL_ACTION, 
    'file' => 'aggregator.admin.inc',

In this case, the access callback is the default (user_access()), and the access arguments are an array containing the string for the permission. The code cannot check more than a permission; if the permissions to check are two, or the conditions to check are not just permissions, then the access callback should be a different one, including a custom one.

node_menu() define some menus that use an access callback different from the default one. The function contains the following code.

  foreach (node_type_get_types() as $type) {
    $type_url_str = str_replace('_', '-', $type->type);
    $items['node/add/' . $type_url_str] = array(
      'title' => $type->name, 
      'title callback' => 'check_plain', 
      'page callback' => 'node_add', 
      'page arguments' => array($type->type), 
      'access callback' => 'node_access', 
      'access arguments' => array('create', $type->type), 
      'description' => $type->description, 
      'file' => 'node.pages.inc',

The function that is defined as access callback (node_access()) is the following one:

function node_access($op, $node, $account = NULL) {
  $rights = &drupal_static(__FUNCTION__, array());

  if (!$node || !in_array($op, array('view', 'update', 'delete', 'create'), TRUE)) {
    // If there was no node to check against, or the $op was not one of the
    // supported ones, we return access denied.
    return FALSE;
  // If no user object is supplied, the access check is for the current user.
  if (empty($account)) {
    $account = $GLOBALS['user'];

  // $node may be either an object or a node type. Since node types cannot be
  // an integer, use either nid or type as the static cache id.

  $cid = is_object($node) ? $node->nid : $node;

  // If we've already checked access for this node, user and op, return from
  // cache.
  if (isset($rights[$account->uid][$cid][$op])) {
    return $rights[$account->uid][$cid][$op];

  if (user_access('bypass node access', $account)) {
    $rights[$account->uid][$cid][$op] = TRUE;
    return TRUE;
  if (!user_access('access content', $account)) {
    $rights[$account->uid][$cid][$op] = FALSE;
    return FALSE;

  // We grant access to the node if both of the following conditions are met:
  // - No modules say to deny access.
  // - At least one module says to grant access.
  // If no module specified either allow or deny, we fall back to the
  // node_access table.
  $access = module_invoke_all('node_access', $node, $op, $account);
  if (in_array(NODE_ACCESS_DENY, $access, TRUE)) {
    $rights[$account->uid][$cid][$op] = FALSE;
    return FALSE;
  elseif (in_array(NODE_ACCESS_ALLOW, $access, TRUE)) {
    $rights[$account->uid][$cid][$op] = TRUE;
    return TRUE;

  // Check if authors can view their own unpublished nodes.
  if ($op == 'view' && !$node->status && user_access('view own unpublished content', $account) && $account->uid == $node->uid && $account->uid != 0) {
    $rights[$account->uid][$cid][$op] = TRUE;
    return TRUE;

  // If the module did not override the access rights, use those set in the
  // node_access table.
  if ($op != 'create' && $node->nid) {
    if (module_implements('node_grants')) {
      $query = db_select('node_access');
      $query->condition('grant_' . $op, 1, '>=');
      $nids = db_or()->condition('nid', $node->nid);
      if ($node->status) {
        $nids->condition('nid', 0);
      $query->range(0, 1);

      $grants = db_or();
      foreach (node_access_grants($op, $account) as $realm => $gids) {
        foreach ($gids as $gid) {
            ->condition('gid', $gid)
            ->condition('realm', $realm)
      if (count($grants) > 0) {
      $result =  (bool) $query
      $rights[$account->uid][$cid][$op] = $result;
      return $result;
    elseif (is_object($node) && $op == 'view' && $node->status) {
      // If no modules implement hook_node_grants(), the default behavior is to
      // allow all users to view published nodes, so reflect that here.
      $rights[$account->uid][$cid][$op] = TRUE;
      return TRUE;

  return FALSE;

There are three points to notice:

  • The arguments declared with "access arguments" will be passed to the function in the same order; the function uses a third parameter because it is not used only access callback.
  • The function returns TRUE if the user has access to the menu, and FALSE if the user doesn't have access to the menu.
  • An access callback can also be used when a menu should be shown only in specific circumstances.
  • When declaring a custom access callback function, it appears it must live in your .module file, because Drupal can't seem to find it in the file declaration (at least for me). Dec 24, 2014 at 14:53

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.