I am working on a server where more than one user has ssh access, and there is more than one project. Only one user has root access, other users are restricted to specific projects.

So, here is how the permissions are configured. All files have group read and write access.

project_A/* is in group_A
project_A/sites/sitename/files/* is in group www-data
project_B/* is in group_B
project_B/sites/sitename/files/* is in group www-data

www-data user is NOT in any other group. Only the files directory can be written on by the www-data user.
So far, so good.

Problem:
Each time user_1 does drush up or drush dl, the newly downloaded or overridden files in project_A get owner=user_1 and group=user_1. The owner is not a problem, but group=user_1 means that other users in group_A can no longer edit those files.

Solution:
It would be very sweet if there was a solution to automatically chgrp those added or updated files back to project_A (or a group name specified in a settings file) after drush dl or drush up.

Any idea how to implement this in drush? Or should this be done outside of drush, in custom shell scripts?
Thanks.

Comments

donquixote’s picture

Thinking about it, it would be even better to have
project_A/sites/sitename/files/* in group www-data-project_A,
and the www-data user being a member of this group. Or the owner should be www-data, and the group should be group_A ..

Problem is that every operation of any user can mess up groups and owners. Not just drush, but also normal file copy, ftp upload, web upload etc.

donquixote’s picture

moshe weitzman’s picture

First, its trivial to use a POST hook to run code after any dl or updatecode command. See the API docs. Next, you might consider a bash alias for drush such as su - wwwdata -c drush. I haven't thought much about that. I think perms is pretty site specific and the best we're gonna do is point people to blog posts and such where folks are sharing their own code.

Slovak’s picture


Problem:
Each time user_1 does drush up or drush dl, the newly downloaded or overridden files in project_A get owner=user_1 and group=user_1. The owner is not a problem, but group=user_1 means that other users in group_A can no longer edit those files.

This can be done with permissions by setting the suid bit on directories. Assuming you are in the directory above your projects directory:

find ./project_A -type d -exec chmod g+s {} \;

All files and directories made after this within project_A will have the same group as project_A.

You'll have to set the user:group on the site/files directory separately, as www-data needs to be able to write to it. I would suggest setting the group to www-data and adding your users into that group. Unfortunately, that opens access for other users into other site/files directories as well - though they are technically exposed by Drupal to the public.

I've tried a similar approach to setting up another group www-data-project_A and adding www-data user to it. The problem is that the web server will not respect the suid on the files directory, therefore any new files created through the web site (i.e. uploaded file) will be www-data:www-data which will make it inaccessible to the people working on the project. Some will say that the sites/files directory should be off limits to developers, but I haven't found that to be practical either (especially since some modules like CiviCRM place their custom and template files within sites/files directory).

I am still looking for the most optimal solution.

Anonymous’s picture

This could be solved by removing the call to rename in drush_move_dir(). If the rename doesn't execute, drush_move_dir() falls back to using drush_copy_dir and drush_delete_dir.
The copy command respects the SETGID bit (chmod g+s) on the parent directory, thus changing the group id on the new files and directories. The move command leaves the group id as it was in the temporary directory.

jm.federico’s picture

Priority: Normal » Major

I'm scaling this to major, security perms are crucial in multi-user environments.

I like @g.idema idea, removing the call to drush_move_dir solves the problems.
Probably this can be made a parameter, so that we can instruct Drush to use copy/delete instead of move.
Somthing like this

drush @alias dl --copy alias

Wouldn't know how to do that, :( can't provide a patch.

Cheers

greg.1.anderson’s picture

Priority: Major » Normal

See #3; patches welcome, but file perms are site-specific, and therefore a bit beyond the scope of drush core. Un-setting 'major'.

weseze’s picture

I'm not very experienced in *nix file permissions, owners and groups. But I don't think that drush should ever change the original owner and group of any file or any directory that it updates.
If my server tools have setup proper permissions, created users and groups and so on, then Drush shouldn't change any of that, regardless of what user I'm currently logged in as.

If I understand #5 correctly then it is possible to fix this and it really is a bug in Drush and not a problem with us - the user - not understanding owners and groups or not having it setup correctly.

Just my opinion on the matter ;)

blackspiraldancer’s picture

To me that's a major issue. I had to put an alias to chown www-data:www-data -R /var/www/whatever because all of the drush commands were resetting (as expected) user and group to root.
Isn't there any way to accept in the command line a valid system user and run drush with a (eg) su -m www-data -c <command> prefix?

moshe weitzman’s picture

@blackspiraldancer - I answered your question in #3. Use a bash alias for drush.

anarcat’s picture

Title: "drush up" and "drush dl" change group and owner for module files » drush dl should change group and owner for module files
Component: Miscellaneous » PM (dl, en, up ...)
Category: feature » support

I do not think drush should mess around with permissions. It makes drush less portable, and less understandable. How will we know which group to use? Which permission?

Now, you are asking how *you* can do this, and that's actually fairly simple. Aegir does this all the time.

You just need to drop a foo.drush.inc file in your .drush directory with the following:

function drush_foo_post_dl() {
  system("chown www-data files/");
}

or similar.

If this needs to be fixed at all, it's only that files should be downloaded in place instead of a tmp directory, so it follows the setuid settings. Running chmod or chown is just bound to fail.

anarcat’s picture

I have opened the feature request #1168812: Respect filesystem permissions about this, so that files are downloaded in place.

jonhattan’s picture

Status: Active » Closed (works as designed)

Per #4 and #1168812-6: Respect filesystem permissions. Group is respected as far as you configure parent directories with g+s, that's out of drush scope.

@Skavare you can set webserver's user umask to 002. Note also that drupal 7 does a chmod of files and dirs, by default to 0775 and 0664. See http://api.drupal.org/api/drupal/includes--file.inc/function/drupal_chmod/7

JordanMagnuson’s picture

Title: drush dl should change group and owner for module files » How to change group owner after dl?
Status: Closed (works as designed) » Active

Could someone help me out with this? I'm incredibly confused/frustrated.

I'm a pretty experienced web developer, with limited sysadmin/Linux experience.

I used to use drush as root, and then discovered that I shouldn't do that. So I set up a user for myself ("magjor") and added this user to the www-data group.

Now when I use drush as magjor to download modules etc (via "dl"), the module folders end up owned by magjor:magjor, instead of magjor:www-data, which means that Apache can not read the files.

I've been looking around for the last couple of hours trying to figure out how best to deal with this, without a whole lot of success. This thread is somewhat helpful, but it seems that people are throwing around a variety of solutions, none of which are explained fully, and all of which seem to have various "issues."

I'd rather not use the bash alias solution, as it seems like a pain to enter a password every time I do something with drush. Seems preferable to have drush just set permissions manually after a dl, as per #11, but I don't understand how to implement this solution.

I've created a file called group_chown.drush.inc and placed it in /home/magjor/.drush . Inside the file I have:

function group_chown_post_dl() {
  system("chown -R magjor:www-data /srv/www/pixelscrapper.com/public_html/sites/all/modules/");
}

But this doesn't seem to do anything. I'm sure I'm doing something wrong.

Any help is much appreciated!

greg.1.anderson’s picture

To patch in after pm-download, you must name your hook after the long command name (pm-download), not its shortcut (dl). In other words, you want:

function drush_group_chown_post_pm_download($project_list) {
  var_export(func_get_args());
}

Immediately after you create your group_chown.drush.inc file, it won't work; you need to run drush cc drush to clear the Drush commandfile cache before your new commandfile is recognized. After you do that, your code will run, and you could put in the code from #14, and it would work.

That is, however, perhaps not the best way, as the only arguments you get in the post pm-download hook is the list of project names downloaded. A better hook to implement is drush_pm_post_download, which gives you the full $request and $release data structures. $request will tell you where Drush placed each download file:

function group_chown_drush_pm_post_download($request, $release) {
  $result = drush_shell_exec("chgrp -R www-data %s", $request['project_install_location']);
  drush_print(implode("\n", drush_shell_exec_output()));
  if (!$result) {
    drush_log(dt('Could not set group of download file.'), 'error');
  }
}

The 'drush_print' line shows you the output for the chgrp operation; this should be empty if everything works okay. If you get 'operation not permitted', note that you need to be a member of the target group in order for this to work.

Marking this support request as 'fixed'; we could potentially put the example above in the examples folder, or perhaps include it with the related issue #1343892: Add a `drush topic` entry for best practice in permissions using drush between production and development.

greg.1.anderson’s picture

Status: Active » Fixed
greg.1.anderson’s picture

I should also add, though, that if you want to solve your permissions issues after dl per #15, you might also want to make similar adjustments in a pm_post_update hook, as pm-updatecode does not call pm-download to update projects.

greg.1.anderson’s picture

JordanMagnuson’s picture

Status: Fixed » Active

Thanks a lot for the help Greg! It would definitely be great to have more documentation regarding permissions and Drush. Drush itself works like a charm, but the permissions situation has made everything fairly confusing (to my mind).

Your example helped immensely. After messing around a bit further, I've created a post_dl hook that changes permissions as well as owner for downloaded files. It seems to work as desired (see below: does it look okay?).

As per your suggestion, I tried creating function owner_perms_drush_pm_post_update($request, $release) as well (in the same file), and pasting the same code into it, but it doesn't seem to be doing the trick (permissions are not changed after performing pm-update). Does that hook need to be handled differently somehow?

Here's my owner_perms_drush_pm_post_download():

define("DRUSH_DIR_PERMS",     "750");
define("DRUSH_FILE_PERMS",    "640");
define("DRUSH_OWNER_USER",    "magjor");
define("DRUSH_OWNER_GROUP",   "www-data");

function owner_perms_drush_pm_post_download($request, $release) {
  // Change owner
  $owner = drush_shell_exec("chown -R %s:%s %s", DRUSH_OWNER_USER, DRUSH_OWNER_GROUP, $request['project_install_location']);
  drush_print(implode("\n", drush_shell_exec_output()));
  if (!$owner) {
    drush_log(dt('Could not set group of download directory.'), 'error');
  }

  // Change permissions
  $parent = drush_shell_exec("chmod %s %s", DRUSH_DIR_PERMS, $request['project_install_location']);
  drush_print(implode("\n", drush_shell_exec_output()));
  if (!$parent) {
    drush_log(dt('Could not change parent directory permissions'), 'error');
  }

  $directories = drush_shell_exec("find %s -type d -exec chmod %s {} \\;", $request['project_install_location'], DRUSH_DIR_PERMS);
  drush_print(implode("\n", drush_shell_exec_output()));
  if (!$directories) {
    drush_log(dt('Could not change directory permissions'), 'error');
  }

  $files = drush_shell_exec("find %s -type f -exec chmod %s {} \\;", $request['project_install_location'], DRUSH_FILE_PERMS);
  drush_print(implode("\n", drush_shell_exec_output()));
  if (!$files) {
    drush_log(dt('Could not change file permissions'), 'error');
  }
}
greg.1.anderson’s picture

Status: Active » Fixed

The post-update hook should be declared like this:

function group_chown_pm_post_update($name, $release_info) {
  drush_print($name);
  var_export($info);
}

Unfortunately, the post-update hook is not very useful for this purpose, as it does not pass you the location of the updated project. That information must be recovered from the DRUSH_PM_UPDATED context. This context contains an array with info on all of the projects that were updated; unfortunately, the items in this array are not keyed by project name, so you have to search through the whole record to find the one you want. If you're going to do that, you might as well just process all of the updated projects in the post-pm-updatecode hook:

function drush_group_chown_post_pm_updatecode($request) {
  $updated = drush_get_context('DRUSH_PM_UPDATED');
  foreach ($updated as $info) {
    drush_print("Updated project " . $info['name'] . " at location " . $info['full_project_path']);
  }
}

Insert your code there, and it should work just fine. Finally, drush_log(dt('message'), 'error'); is lazy debug code. Don't follow my example in your actual hooks; always abort on errors with return drush_set_error('ERROR_CODE_YOU_INVENT', dt('message'));.

JordanMagnuson’s picture

Ah, thank you--you've been a miracle-worker Greg! Here's the final code I'm using, which has made my life much, much happier.

(The only thing I'm still a bit confused about is drush_print(implode("\n", drush_shell_exec_output())); --> is that line necessary as I have put it in, below, or can I leave it out in my use case?)

define("DRUSH_DIR_PERMS",     "750");
define("DRUSH_FILE_PERMS",    "640");
define("DRUSH_OWNER_USER",    "magjor");
define("DRUSH_OWNER_GROUP",   "www-data");

/**
 * Implements hook_drush_pm_post_download()
 */
function owner_perms_drush_pm_post_download($request, $release) {
  // Change owner
  _owner_perms_change_owner($request['project_install_location']);

  // Change permissions
  _owner_perms_change_perms($request['project_install_location']);
}

/**
 * Implements drush_hook_post_pm_updatecode()
 */
function drush_owner_perms_post_pm_updatecode($request) {
  $updated = drush_get_context('DRUSH_PM_UPDATED');
  foreach ($updated as $info) {
    // Change owner
    _owner_perms_change_owner($info['full_project_path']);

    // Change permissions
    _owner_perms_change_perms($info['full_project_path']);
  }
} 

function _owner_perms_change_owner($path) {
  $owner = drush_shell_exec("chown -R %s:%s %s", DRUSH_OWNER_USER, DRUSH_OWNER_GROUP, $path);
  drush_print(implode("\n", drush_shell_exec_output()));
  if (!$owner) {
    return drush_set_error('OWNER_PERMS', dt("Could not update directory owner at " . $path));
  }

  drush_print("Updated directory owner at " . $path);
}

function _owner_perms_change_perms($path) {
  // Parent directory
  $parent = drush_shell_exec("chmod %s %s", DRUSH_DIR_PERMS, $path);
  drush_print(implode("\n", drush_shell_exec_output()));
  if (!$parent) {
    return drush_set_error('OWNER_PERMS', dt("Could not update parent directory permissions at " . $path));
  }

  // Child directories
  $directories = drush_shell_exec("find %s -type d -exec chmod %s {} \\;", $path, DRUSH_DIR_PERMS);
  drush_print(implode("\n", drush_shell_exec_output()));
  if (!$directories) {
    return drush_set_error('OWNER_PERMS', dt("Could not update child directory permissions at " . $path));
  }

  // Files
  $files = drush_shell_exec("find %s -type f -exec chmod %s {} \\;", $path, DRUSH_FILE_PERMS);
  drush_print(implode("\n", drush_shell_exec_output()));
  if (!$files) {
    return drush_set_error('OWNER_PERMS', dt("Could not update file permissions at " . $path));
  }

  drush_print("Updated directory and file permissions at " . $path);
}
greg.1.anderson’s picture

drush_print(implode("\n", drush_shell_exec_output())); dumps the output of the previous command, which is usually empty. Any reported error messages will show up here, so I think it's useful to leave it in, just in case.

Thanks for the hook implementations; those might make a good addition to the 'examples' folder. Already tracking this issue from #1343892: Add a `drush topic` entry for best practice in permissions using drush between production and development.

JordanMagnuson’s picture

Title: How to change group owner after dl? » How to change permissions and owner after dl / update?

Just changing title to be more clear.

Status: Fixed » Closed (fixed)

Automatically closed -- issue fixed for 2 weeks with no activity.

ShaunDychko’s picture

Version: » 7.x-5.8

Thanks a lot for sharing this Jordan. Another piece of the puzzle is to put the code from #21 in a file named owner_perms.drush.inc in the ~/.drush directory.

Slovak’s picture

Assuming you are dl into project_A/modules for example. Make sure that this modules directory has permissions g+s (2775) and group=project_A

Add the desired users to project_A group.

drush dl as desired, no need for chgrp.

* Note that setting the setgid permission on a directory only affects the group ID of new files and subdirectories created after the setgid bit is set, and is not applied to existing entities.

ShaunDychko’s picture

Here's an update to #21 that adds support for updating Drupal core. It search for all sites/*/files directories and sets 770 directory permissions and 660 files permissions recursively. This patch also removes the $results parameter from the hook implementation since it isn't necessary according to http://api.drush.org/api/drush/docs%21drush.api.php/function/drush_hook_..., and it was throwing an error message.

<?php
define("DRUSH_DIR_PERMS",     "750");
define("DRUSH_FILE_PERMS",    "640");
define("DRUSH_OWNER_USER",    "sdychko");
define("DRUSH_OWNER_GROUP",   "www");
define("DRUSH_DIR_PERMS_FILES", "770");
define("DRUSH_FILE_PERMS_FILES", "660");

/**
 * Implements hook_drush_pm_post_download()
 */
function owner_perms_drush_pm_post_download($request, $release) {
  // Change owner
  _owner_perms_change_owner($request['project_install_location']);

  // Change permissions
  _owner_perms_change_perms($request['project_install_location']);
}

/**
 * Implements drush_hook_post_pm_updatecode()
 */
function drush_owner_perms_post_pm_updatecode() {
  $updated = drush_get_context('DRUSH_PM_UPDATED');
  foreach ($updated as $info) {
    if($info['name'] == 'drupal') {
      // It's Drupal core!
      _owner_perms_change_owner($info['base_project_path']);
      _owner_perms_change_perms($info['base_project_path']);
      // Give server write permissions to files directories and files.
      $results = scandir($info['base_project_path'] . '/sites');
      $files_directories = array();
      foreach ($results as $result) {
        if ($result === '.' || $result === '..') continue;
        if (is_dir($info['base_project_path'] . '/sites/' . $result . '/files')) {
          $files_directories[] = $info['base_project_path'] . '/sites/' . $result . '/files';
        }
      }
      foreach ($files_directories as $path) {
        _owner_perms_change_perms_files_dir($path);
      }
    }
    else {
      // It's a contributed module.
      // Change owner
      _owner_perms_change_owner($info['full_project_path']);

      // Change permissions
      _owner_perms_change_perms($info['full_project_path']);
    }
  }
} 

/**
 * Change the permissions on files directories to allow
 * write permission for the server.
 */
function _owner_perms_change_perms_files_dir($path) {
  $directories = drush_shell_exec("find %s -type d -exec chmod %s {} \\;", $path, DRUSH_DIR_PERMS_FILES);
  drush_print(implode("\n", drush_shell_exec_output()));
  if (!$directories) {
    return drush_set_error('OWNER_PERMS', dt("Could not update directory permissions at " . $path));
  }
  drush_print("Updated directory permissions at " . $path);
  $files = drush_shell_exec("find %s -type f -exec chmod %s {} \\;", $path, DRUSH_FILE_PERMS_FILES);
  drush_print(implode("\n", drush_shell_exec_output()));
  if (!$files) {
    return drush_set_error('OWNER_PERMS', dt("Could not update directory permissions at " . $path));
  }
  drush_print("Update file permissions at " . $path);
}

function _owner_perms_change_owner($path) {
  $owner = drush_shell_exec("chown  -R %s:%s %s", DRUSH_OWNER_USER, DRUSH_OWNER_GROUP, $path);
  drush_print(implode("\n", drush_shell_exec_output()));
  if (!$owner) {
    return drush_set_error('OWNER_PERMS', dt("Could not update directory owner at " . $path));
  }

  drush_print("Updated directory owner at " . $path);
}

function _owner_perms_change_perms($path) {
  // Parent directory
  $parent = drush_shell_exec("chmod  %s %s", DRUSH_DIR_PERMS, $path);
  drush_print(implode("\n", drush_shell_exec_output()));
  if (!$parent) {
    return drush_set_error('OWNER_PERMS', dt("Could not update parent directory permissions at " . $path));
  }

  // Child directories
  $directories = drush_shell_exec("find %s -type d -exec chmod  %s {} \\;", $path, DRUSH_DIR_PERMS);
  drush_print(implode("\n", drush_shell_exec_output()));
  if (!$directories) {
    return drush_set_error('OWNER_PERMS', dt("Could not update child directory permissions at " . $path));
  }

  // Files
  $files = drush_shell_exec("find %s -type f -exec chmod  %s {} \\;", $path, DRUSH_FILE_PERMS);
  drush_print(implode("\n", drush_shell_exec_output()));
  if (!$files) {
    return drush_set_error('OWNER_PERMS', dt("Could not update file permissions at " . $path));
  }

  drush_print("Updated directory and file permissions at " . $path);
}

note also that a more complicated solution to this issue it being worked on here: http://drupal.org/node/990812