8.5.x node.api.php hook_node_access(\Drupal\node\NodeInterface $node, $op, \Drupal\Core\Session\AccountInterface $account)
8.0.x node.api.php hook_node_access(\Drupal\node\NodeInterface $node, $op, \Drupal\Core\Session\AccountInterface $account)
8.1.x node.api.php hook_node_access(\Drupal\node\NodeInterface $node, $op, \Drupal\Core\Session\AccountInterface $account)
8.2.x node.api.php hook_node_access(\Drupal\node\NodeInterface $node, $op, \Drupal\Core\Session\AccountInterface $account)
8.3.x node.api.php hook_node_access(\Drupal\node\NodeInterface $node, $op, \Drupal\Core\Session\AccountInterface $account)
8.4.x node.api.php hook_node_access(\Drupal\node\NodeInterface $node, $op, \Drupal\Core\Session\AccountInterface $account)
8.6.x node.api.php hook_node_access(\Drupal\node\NodeInterface $node, $op, \Drupal\Core\Session\AccountInterface $account)
7.x node.api.php hook_node_access($node, $op, $account)

Control access to a node.

Modules may implement this hook if they want to have a say in whether or not a given user has access to perform a given operation on a node.

The administrative account (user ID #1) always passes any access check, so this hook is not called in that case. Users with the "bypass node access" permission may always view and edit content through the administrative interface.

Note that not all modules will want to influence access on all node types. If your module does not want to actively grant or block access, return NODE_ACCESS_IGNORE or simply return nothing. Blindly returning FALSE will break other node access modules.

Also note that this function isn't called for node listings (e.g., RSS feeds, the default home page at path 'node', a recent content block, etc.) See Node access rights for a full explanation.


$node: Either a node object or the machine name of the content type on which to perform the access check.

$op: The operation to be performed. Possible values:

  • "create"
  • "delete"
  • "update"
  • "view"

$account: The user object to perform the access check operation on.

Return value

Related topics

1 function implements hook_node_access()

Note: this list is generated by pattern matching, so it may include some functions that are not actually implementations of this hook.

node_node_access in modules/node/node.module
Implements hook_node_access().
1 invocation of hook_node_access()
node_access in modules/node/node.module
Determines whether the current user may perform the operation on the node.


modules/node/node.api.php, line 608
Hooks provided by the Node module.


function hook_node_access($node, $op, $account) {
  $type = is_string($node) ? $node : $node->type;
  if (in_array($type, node_permissions_get_configured_types())) {
    if ($op == 'create' && user_access('create ' . $type . ' content', $account)) {
      return NODE_ACCESS_ALLOW;
    if ($op == 'update') {
      if (user_access('edit any ' . $type . ' content', $account) || user_access('edit own ' . $type . ' content', $account) && $account->uid == $node->uid) {
        return NODE_ACCESS_ALLOW;
    if ($op == 'delete') {
      if (user_access('delete any ' . $type . ' content', $account) || user_access('delete own ' . $type . ' content', $account) && $account->uid == $node->uid) {
        return NODE_ACCESS_ALLOW;

  // Returning nothing from this function would have the same effect.


kiamlaluno’s picture

Before Drupal 7, Drupal defined the hook hook_access(); differently from hook_node_access(), hook_access() is thought to be implemented from modules to limit access to the node types they define.

fuerst’s picture

Important note when considering using this hook:

hook_node_access() only kicks in when trying to view the full node. It doesn't prevent the rendering of menu items, nor does it prevent teasers and any other views than the full node view.

What you need to do is implement hook_node_access_records() and hook_node_grants(). This will prevent both the menu items from being rendered, as well as prevent teasers and any other views being rendered, including views created by the Views module and any other modules.

(Citing Jay Matwichuk)

davidwhthomas’s picture

Actually hook_node_access does prevent the rendering of menu items. node_menu uses node_access as the access callback. node_access invokes hook_node_access. See node_access for the details. You may need to clear menu cache to take effect.

anandkp’s picture

Four years and many D7 versions down the line (version 7.54 as of writing this) and I've found that NO, using hook_node_access() to deny access to a particular Node (based on the logged in User's role) does not remove the denied Node's menu item from menus.

Here's my hook_node_access() implementation:

function mymodule_node_access($node, $op, $account) {
  $is_anonymous = !user_is_logged_in(); // Drupal Core User module.
  $is_client = _mymodule_user_is_client($account); // Custom role check helper function.
  $is_admin = _mymodule_user_is_admin($account); // Custom role check helper function.

  // $op agnostic Node Access restrictions.
  if (is_object($node)) {
    switch ($node->nid) {
      // Disable access to FAQ page for Client users.
      case 58:
        if ($is_anonymous || $is_client) {
          return NODE_ACCESS_DENY;


The aforementioned code worked for preventing the Anonymous and Client role users from VIEWING the page. That said, I faced the same problem mentioned and cited by fuerst in #24158 earlier in this thread... I'd created a custom menu using the Menu UI and added a menu item for the node in question. I expected that my hook_node_access() implementation would hide the menu item as well as prevent the node from being viewed by Anonymous and Client Role Users – it didn't.

Here's what worked for me (hook_node_grants() and hook_node_access_records()):

 * Constants necessary for customised Node Access permission alteration.
 * @see https://www.drupal.org/node/1260948
// Realm name.
define('MYMODULE_NODE_ACCESS_REALM', 'mymodule');
// Grant IDs to be used for per-role Grants.

 * Implements hook_node_grants().
 * @see https://www.drupal.org/node/1260948
function MYMODULE_node_grants($account, $op) {
  // Initialise $grants array with REALM ID, "mymodule".
  $grants = array(MYMODULE_NODE_ACCESS_REALM => array());

  // A convenience array keyed by ROLE names (note how they are the
  // used as the suffixes of the Constants we defined above? e.g. 
  // "MYMODULE_GRANT_ID_ANONYMOUS") with a (boolean) value that indicates
  // whether the logged in User has this role.
  $grant_roles = array(
    'ANONYMOUS'     => !user_is_logged_in(),
    'CLIENT'        => _mymodule_user_is_client($account),

  // We are only concerned with the Node VIEW operation at present.
  // Update the logged in User's grant IDs for our custom "REALM"
  // by looping over the $grant_roles above.
  if ($op === 'view') {
    foreach ($grant_roles as $role_suffix => $role_status) {
      if ($role_status) {
        $grant_constant = "MYMODULE_GRANT_ID_{$role_suffix}";
        $grants[MYMODULE_NODE_ACCESS_REALM][] = $grant_constant;

  return $grants;

 * Implements hook_node_access_records().
 * @see https://www.drupal.org/node/1260948
function mymodule_node_access_records($node) {
  $grants = array();

  // Set node access for FAQ page (node/58):
  if ($node->nid === 58) {
    // Remove ALL node-op permissions for Anonymous users.
    $grants[] = array(
      'grant_view' => 0,
      'grant_update' => 0,
      'grant_delete' => 0,
      'priority' => 0,
    // Remove ALL node-op permissions for Client users.
    $grants[] = array(
      'grant_view' => 0,
      'grant_update' => 0,
      'grant_delete' => 0,
      'priority' => 0,

  return $grants;

I hope this info proves useful!

reptilex’s picture

It should be obvious, but sometimes you look for hours and miss these things.

I did everything right but never saw the function even being called, after a while I discovered I was logged in as admin, so the part where hook_node_access implementations are checked was being ignored. (node_access function)

if (user_access('bypass node access', $account)) {
    $rights[$account->uid][$cid][$op] = TRUE;
    return TRUE;
xcafebabe’s picture

Thank for this comment! You saved me a headache!

zsuffad’s picture


aegagros’s picture

This thing had be stuck for hours. I wrote a custom hook_node_access() based on a field attached to the $user object. While the devel module (node access by user) reported that user X (not an administrator) had FULL update access to a node, I couldn't see the EDIT tab on the node page when I was logged on as that user, and the node/%/edit path gave me an 'access denied' message. The reason was that when the devel module checks access per user, it loads the full $user object and passes it to the node_access() function, which makes my custom hook work properly and gives me the right result on the devel block. However, the default node_access() call is made using the global $user object which is not a complete object, as reported by comment 5634806, therefore my function failed. In order to use fields inside this hook, you probably have to load the complete $user object using something like this:

// do not name it $user as it may conflict with the global $user variable
$user_obj = user_load($account->uid);
axiom3279’s picture

Thank you, I had same issue you saved me several hours of work.

orbmantell’s picture

I have a site that I would rather redirect a user than give an access denied page, but I cannot use drupal_goto() in hook_node_view() (The user shouldn't fire the node view handler because that triggers various rules I have set up). Is it appropriate to use drupal_goto() inside hook_node_access(), or will this cause issues when nothing is returned from the function (or for some other reason)?

ronsnow’s picture

The problem I ran into in using drupal_goto() in hook_node_access is that it appeared to generate errors when cron ran. I started getting "Warning: Cannot modify header information - headers already sent by..." errors due to the drupal_goto.

My cron hook does periodic notifications to certain users about the content they control and thus was accessing nodes in the process.

That seems to make sense, I was using the goto to redirect users to the home page with a drupal message displaying of the reason. Going to the home page doesn't make sense to cron jobs.

bigscotia10’s picture

May be helpful if you are trying to prevent user access to a content types nodes based on role.

For example:

To set role level permissions on viewing content from a specific content type, use Hook_node_access.

For example:

// Add perms to the super cool content type
function HOOK_node_access($node, $op, $account) {
  $type = is_string($node) ? $node : $node->type;

  // if its a super cool content type, user content types machine name.
  if ($type == 'super_cool' && $op == 'view') {
    // Define roles that can see super cool content type content.
   global $user;
   $super_cool_perms = array_intersect(
     'role name', 
     'role name 2', 
     'role name 3',
     'role name 4', 
   // If they have any of these roles, then they can see the super cool content
   if ($super_cool_perms){
      return NODE_ACCESS_ALLOW;
    else {
      return NODE_ACCESS_DENY;
olorin.fr’s picture

Inspired a new version of your example and node_access_example.module:

function HOOK_node_access($node, $op, $account){
    if (is_string($node))
        return NODE_ACCESS_IGNORE;

    if ($node->type == 'my_type' && $op == 'view'){
        $allowed_roles = array(
            'authenticated user'
        $access_allow = array_intersect($allowed_roles, array_values($account->roles));
        if ($access_allow)
            return NODE_ACCESS_ALLOW;
            return NODE_ACCESS_DENY;
xia325again’s picture

Why and when the $node could be a sting type?If this is string type, how can I use it to get a $node->uid

leanderl’s picture

bigscotia10, thanks for the clear and usable example. It solved my task right away. The documentation on d.o is sometimes hard to follow without practical examples. Thanks for providing one.

rimu’s picture

NB that hook_node_access seems to have a dual purpose - it is called once for each content type, with $node set to the machine name of each content type AND it is called with $node being a node object which is being viewed, edited, etc.

laiserosa’s picture

I'm new in Drupal (and English too haha). I'm trying create a type of structure and I tested Maestro, Trigger, Workflow and Rules modules, but none them help me completely. What I want is: When I finish a content, after updating a few times, I would like to close edition by the end date, and only a particular user had permission to reopen the content type to be edited. After close to update content, it would be possible to automate permission for the user to update other content (or webform)? Example: I create a content of content type A and update with others users. After a certain date, it closes and can no longer be edited. After closing, you can edit content of content type B and same for content type C. With webform I can close and it's a formulary, it would better, but content type also useful.
I created a flow in maestro, but I don't know how use it: http://i.imgsafe.org/e8c7b10.jpg .
I understood in a part needs a "batch function", however I don't know where to place this code in Drupal.
Thanks for help!

daka’s picture

I wrote a custom hook_node_access() in my module, and it was working well. But when I changed hook_node_access() code, it did not work anymore. It seems that drupal ignore this code after any change. After disabling and enabling the module it worked fine again.
Is there any similar experiment?

betoscopio’s picture

This is because the node_access are cached. Disable and enable the module reads the implementation again, clearing the cache does the same.

Bill Choy’s picture

Have the same problem. Here is a easier cheat while developing your code.

You can use the devel module to execute "drupal_static_reset('node_access');", to clear the cache.
The cache was generated by the following code:
function node_access($op, $node, $account = NULL) {
$rights = &drupal_static(__FUNCTION__, array());


Bill Choy’s picture

I'm trying to restrict a role to able to only view nodes that they created.

Looking at the node_access function in the node.module, you have it checks "user_access('access content', $account)" before calling "module_invoke_all('node_access', $node, $op, $account);".

So you need to grant "View published content" and then remove view access via hook_node_access(). Which can be a issue, because of the caching issues noted above. I don't know where/when the cache value ($rights[$account->uid][$cid][$op]) gets cleared.