Browse Source

HUE-720 [useradmin] Add home dir.

User admins should be able to automatically create home directories for users.
Importing users should also automatically create home dirs.
abec 13 năm trước cách đây
mục cha
commit
dd27176c51

+ 2 - 20
apps/useradmin/src/useradmin/templates/list_users.mako

@@ -31,7 +31,7 @@ ${layout.menubar(section='users', _=_)}
             %if user.is_superuser == True:
             <a href="${ url('useradmin.views.edit_user') }" class="btn">${_('Add user')}</a>
             <a href="${ url('useradmin.views.add_ldap_user') }" class="btn">${_('Add/Sync LDAP user')}</a>
-            <a href="#syncLdap" class="btn" data-toggle="modal">${_('Sync LDAP users/groups')}</a>
+            <a href="javascript:void(0)" class="btn confirmationModal" data-confirmation-url="${ url('useradmin.views.sync_ldap_users_groups') }">${_('Sync LDAP users/groups')}</a>
             %endif
         </div>
         <form class="form-search">
@@ -80,25 +80,7 @@ ${layout.menubar(section='users', _=_)}
         </tbody>
     </table>
 
-    <div id="syncLdap" class="modal hide fade">
-        <div class="modal-header">
-            <button type="button" class="close" data-dismiss="modal">&times;</button>
-            <h3>${_('Sync LDAP users and groups')}</h3>
-        </div>
-        <div class="modal-body">
-            <div class="alert alert-info">
-                ${_("This will not import any users or groups that don't already exist in Hue. Only users and groups imported from LDAP can be synced.")}
-                <br/>
-                ${_("All user information and group memberships will be updated based on the LDAP server's current state.")}
-            </div>
-        </div>
-        <div class="modal-footer">
-            <form action="${ url('useradmin.views.sync_ldap_users_groups') }" method="POST">
-                <a href="#" class="btn" data-dismiss="modal">${_('Cancel')}</a>
-                <input type="submit" class="btn primary" value="${_('Sync')}"/>
-             </form>
-        </div>
-    </div>
+    <div id="syncLdap" class="modal hide fade"></div>
 
     <div id="deleteUser" class="modal hide fade"></div>
 

+ 53 - 0
apps/useradmin/src/useradmin/templates/sync_ldap_users_groups.mako

@@ -0,0 +1,53 @@
+## Licensed to Cloudera, Inc. under one
+## or more contributor license agreements.  See the NOTICE file
+## distributed with this work for additional information
+## regarding copyright ownership.  Cloudera, Inc. licenses this file
+## to you under the Apache License, Version 2.0 (the
+## "License"); you may not use this file except in compliance
+## with the License.  You may obtain a copy of the License at
+##
+##     http://www.apache.org/licenses/LICENSE-2.0
+##
+## Unless required by applicable law or agreed to in writing, software
+## distributed under the License is distributed on an "AS IS" BASIS,
+## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+## See the License for the specific language governing permissions and
+## limitations under the License.
+<%!
+from django.utils.translation import ugettext as _
+%>
+
+<%def name="render_field(field)">
+  %if not field.is_hidden:
+    <% group_class = len(field.errors) and "error" or "" %>
+    <label class="control-label" for="id_${field.html_name}">
+      <span>${field.label}</span>
+      ${unicode(field) | n}
+      % if len(field.errors):
+        <span class="help-inline">${unicode(field.errors) | n}</span>
+      % endif
+      &nbsp;
+    </label>
+  %endif
+</%def>
+
+<div class="modal-header">
+  <button type="button" class="close" data-dismiss="modal">&times;</button>
+  <h3>${_('Sync LDAP users and groups')}</h3>
+</div>
+<div class="modal-body">
+  <div class="alert alert-info">
+    ${_("This will not import any users or groups that don't already exist in Hue. Only users and groups imported from LDAP can be synced.")}
+    <br/>
+    ${_("All user information and group memberships will be updated based on the LDAP server's current state.")}
+  </div>
+</div>
+<div class="modal-footer">
+  <form action="${path}" method="POST" class="form form-inline">
+    % for field in form:
+      ${render_field(field)}
+    % endfor
+    <a href="#" class="btn" data-dismiss="modal">${_('Cancel')}</a>
+    <input type="submit" class="btn primary" value="${_('Sync')}"/>
+  </form>
+</div>

+ 83 - 7
apps/useradmin/src/useradmin/tests.py

@@ -23,6 +23,7 @@ Tests for "user admin"
 
 import urllib
 
+from nose.plugins.attrib import attr
 from nose.tools import assert_true, assert_equal, assert_false
 
 from desktop.lib.django_test_util import make_logged_in_client
@@ -34,7 +35,8 @@ from useradmin.models import HuePermission, GroupPermission, LdapGroup, UserProf
 from useradmin.models import get_profile
 
 import useradmin.conf
-from views import sync_ldap_users_and_groups, import_ldap_user, import_ldap_group, \
+from hadoop import pseudo_hdfs4
+from views import sync_ldap_users, sync_ldap_groups, import_ldap_user, import_ldap_group, \
                   add_ldap_user, add_ldap_group, sync_ldap_users_groups
 import ldap_access
 
@@ -243,7 +245,8 @@ def test_user_admin():
 
   reset_all_users()
   reset_all_groups()
-  c = make_logged_in_client(username="test", is_superuser=True)
+
+  c = make_logged_in_client('test', is_superuser=True)
 
   # Test basic output.
   response = c.get('/useradmin/')
@@ -374,7 +377,8 @@ def test_useradmin_ldap_integration():
   assert_true(get_profile(larry).creation_method == str(UserProfile.CreationMethod.EXTERNAL))
 
   # Should be a noop
-  sync_ldap_users_and_groups()
+  sync_ldap_users()
+  sync_ldap_groups()
   assert_equal(len(User.objects.all()), 1)
   assert_equal(len(Group.objects.all()), 0)
 
@@ -427,8 +431,7 @@ def test_add_ldap_user():
   # Set up LDAP tests to use a LdapTestConnection instead of an actual LDAP connection
   ldap_access.CACHED_LDAP_CONN = LdapTestConnection()
 
-
-  c = make_logged_in_client(username='test', is_superuser=True)
+  c = make_logged_in_client('test', is_superuser=True)
 
   assert_true(c.get(URL))
 
@@ -471,8 +474,81 @@ def test_sync_ldap_users_groups():
   # Set up LDAP tests to use a LdapTestConnection instead of an actual LDAP connection
   ldap_access.CACHED_LDAP_CONN = LdapTestConnection()
 
-
-  c = make_logged_in_client(username='test', is_superuser=True)
+  c = make_logged_in_client('test', is_superuser=True)
 
   assert_true(c.get(URL))
   assert_true(c.post(URL))
+
+@attr('requires_hadoop')
+def test_ensure_home_directory_add_ldap_user():
+  URL = reverse(add_ldap_user)
+
+  reset_all_users()
+  reset_all_groups()
+
+  # Set up LDAP tests to use a LdapTestConnection instead of an actual LDAP connection
+  ldap_access.CACHED_LDAP_CONN = LdapTestConnection()
+
+  cluster = pseudo_hdfs4.shared_cluster()
+  c = make_logged_in_client(cluster.superuser, is_superuser=True)
+  cluster.fs.setuser(cluster.superuser)
+
+  assert_true(c.get(URL))
+
+  response = c.post(URL, dict(username='moe', password1='test', password2='test'))
+  assert_true('/useradmin/users' in response['Location'])
+  assert_false(cluster.fs.exists('/user/moe'))
+
+  # Try same thing with home directory creation.
+  response = c.post(URL, dict(username='curly', password1='test', password2='test', ensure_home_directory=True))
+  assert_true('/useradmin/users' in response['Location'])
+  assert_true(cluster.fs.exists('/user/curly'))
+
+  response = c.post(URL, dict(username='bad_name', password1='test', password2='test'))
+  assert_true('Could not' in response.context['form'].errors['username'][0])
+  assert_false(cluster.fs.exists('/user/bad_name'))
+
+  # See if moe, who did not ask for his home directory, has a home directory.
+  assert_false(cluster.fs.exists('/user/moe'))
+
+  # Clean up
+  cluster.fs.rmtree('/user/curly')
+
+@attr('requires_hadoop')
+def test_ensure_home_directory_sync_ldap_users_groups():
+  URL = reverse(sync_ldap_users_groups)
+
+  reset_all_users()
+  reset_all_groups()
+
+  # Set up LDAP tests to use a LdapTestConnection instead of an actual LDAP connection
+  ldap_access.CACHED_LDAP_CONN = LdapTestConnection()
+
+  cluster = pseudo_hdfs4.shared_cluster()
+  c = make_logged_in_client(cluster.superuser, is_superuser=True)
+  cluster.fs.setuser(cluster.superuser)
+
+  response = c.post(reverse(add_ldap_user), dict(username='curly', password1='test', password2='test'))
+  assert_false(cluster.fs.exists('/user/curly'))
+  assert_true(c.post(URL, dict(ensure_home_directory=True)))
+  assert_true(cluster.fs.exists('/user/curly'))
+
+@attr('requires_hadoop')
+def test_ensure_home_directory():
+  reset_all_users()
+  reset_all_groups()
+
+  # Cluster and client for home directory creation
+  cluster = pseudo_hdfs4.shared_cluster()
+  c = make_logged_in_client(cluster.superuser, is_superuser=True)
+  cluster.fs.setuser(cluster.superuser)
+
+  # Create a user with a home directory
+  response = c.post('/useradmin/users/new', dict(username="test1", password1='test', password2='test', ensure_home_directory=True))
+  assert_true(cluster.fs.exists('/user/test1'))
+
+  # Create a user, then add their home directory
+  response = c.post('/useradmin/users/new', dict(username="test2", password1='test', password2='test'))
+  assert_false(cluster.fs.exists('/user/test2'))
+  response = c.post('/useradmin/users/edit/%s' % "test2", dict(username="test2", password1='test', password2='test', ensure_home_directory=True))
+  assert_true(cluster.fs.exists('/user/test2'))

+ 71 - 13
apps/useradmin/src/useradmin/views.py

@@ -32,6 +32,7 @@ from django.core.urlresolvers import reverse
 from django.forms.util import ErrorList
 from django.shortcuts import redirect
 
+from hadoop.fs.exceptions import WebHdfsException
 from useradmin.models import GroupPermission, HuePermission, UserProfile, LdapGroup
 from useradmin.models import get_profile, get_default_user_group
 import ldap_access
@@ -44,7 +45,7 @@ __users_lock = threading.Lock()
 __groups_lock = threading.Lock()
 
 def list_users(request):
-  return render("list_users.mako", request, dict(users=User.objects.all()))
+  return render("list_users.mako", request, dict(users=User.objects.all(), request=request))
 
 def list_groups(request):
   return render("list_groups.mako", request, dict(groups=Group.objects.all()))
@@ -115,9 +116,13 @@ class UserChangeForm(django.contrib.auth.forms.UserChangeForm):
       error_messages = {'invalid': _("Whitespaces and ':' not allowed") })
   password1 = forms.CharField(label=_("Password"), widget=forms.PasswordInput, required=False)
   password2 = forms.CharField(label=_("Password confirmation"), widget=forms.PasswordInput, required=False)
+  ensure_home_directory = forms.BooleanField(label=_("Create Home Directory"),
+                                            help_text=_("Create home directory if one doesn't already exist."),
+                                            initial=True,
+                                            required=False)
 
   class Meta(django.contrib.auth.forms.UserChangeForm.Meta):
-    fields = ["username", "first_name", "last_name", "email"]
+    fields = ["username", "first_name", "last_name", "email", "ensure_home_directory"]
 
   def clean_password2(self):
     password1 = self.cleaned_data.get("password1", "")
@@ -186,7 +191,7 @@ def edit_user(request, username=None):
     form = form_class(request.POST, instance=instance)
     if form.is_valid(): # All validation rules pass
       if instance is None:
-        form.save()
+        instance = form.save()
       else:
         #
         # Check for 3 more conditions:
@@ -215,9 +220,15 @@ def edit_user(request, username=None):
         finally:
           __users_lock.release()
 
+      # Ensure home directory is created, if necessary.
+      if form.cleaned_data['ensure_home_directory']:
+        try:
+          ensure_home_directory(request.fs, instance.username)
+        except (IOError, WebHdfsException), e:
+          request.error(_('Cannot make home directory for user %s' % instance.username))
       return redirect(reverse(list_users))
   else:
-    form = form_class(instance=instance)
+    form = form_class(instance=instance, initial={'ensure_home_directory': False})
   return render('edit_user.mako', request, dict(form=form, action=request.path, username=username))
 
 def edit_group(request, name=None):
@@ -292,6 +303,10 @@ class AddLdapUserForm(forms.Form):
                                     "distinguished name."),
                           initial=False,
                           required=False)
+  ensure_home_directory = forms.BooleanField(label=_("Create Home Directory"),
+                                            help_text=_("Create home directory for user if one doesn't already exist."),
+                                            initial=True,
+                                            required=False)
 
   def clean(self):
     cleaned_data = super(AddLdapUserForm, self).clean()
@@ -325,6 +340,11 @@ def add_ldap_user(request):
       username = form.cleaned_data['username']
       import_by_dn = form.cleaned_data['dn']
       user = import_ldap_user(username, import_by_dn)
+      if form.cleaned_data['ensure_home_directory']:
+        try:
+          ensure_home_directory(request.fs, user.username)
+        except (IOError, WebHdfsException), e:
+          request.error(_("Cannot make home directory for user %s" % user.username))
 
       if user is None:
         errors = form._errors.setdefault('username', ErrorList())
@@ -352,6 +372,10 @@ class AddLdapGroupForm(forms.Form):
                                       help_text=_('Import unimported or new users from the group.'),
                                       initial=False,
                                       required=False)
+  ensure_home_directories = forms.BooleanField(label=_('Create home directories'),
+                                                help_text=_('Create home directories for every member imported, if members are being imported.'),
+                                                initial=True,
+                                                required=False)
 
   def clean(self):
     cleaned_data = super(AddLdapGroupForm, self).clean()
@@ -409,10 +433,32 @@ def sync_ldap_users_groups(request):
     raise PopupException(_("You must be a superuser to sync the LDAP users/groups."))
 
   if request.method == 'POST':
-    sync_ldap_users_and_groups()
-    return redirect(reverse(list_users))
-  else:
-    raise PopupException(_("POST request required in order to sync the LDAP users/groups."))
+    form = SyncLdapUsersGroupsForm(request.POST)
+    if form.is_valid():
+      users = sync_ldap_users()
+      groups = sync_ldap_groups()
+
+      # Create home dirs for every user sync'd
+      if form.cleaned_data['ensure_home_directory']:
+        for user in users:
+          try:
+            ensure_home_directory(request.fs, user.username)
+          except (IOError, WebHdfsException), e:
+            raise PopupException(_("The import may not be complete, sync again"), detail=e)
+      return redirect(reverse(list_users))
+
+  form = SyncLdapUsersGroupsForm()
+  return render("sync_ldap_users_groups.mako", request, dict(path=request.path, form=form))
+
+def ensure_home_directory(fs, username):
+  """
+  Adds a users home directory if it doesn't already exist.
+
+  Throws WebHdfsException.
+  """
+  home_dir = '/user/%s' % username
+  if not fs.exists(home_dir):
+    fs.create_home_dir(home_dir)
 
 def _check_remove_last_super(user_obj):
   """Raise an error if we're removing the last superuser"""
@@ -570,21 +616,29 @@ def import_ldap_user(user, import_by_dn):
 def import_ldap_group(group, import_members, import_by_dn):
   return _import_ldap_group(group, import_members, import_by_dn)
 
-def sync_ldap_users_and_groups():
+def sync_ldap_users():
   """
-  Syncs LDAP user information and group memberships. This will not import new
-  users or groups from LDAP. It is also not possible to import both a user and a
+  Syncs LDAP user information. This will not import new
+  users from LDAP. It is also not possible to import both a user and a
   group at the same time. Each must be a separate operation. If neither a user,
   nor a group is provided, all users and groups will be synced.
   """
-  # Sync everything
   users = User.objects.filter(userprofile__creation_method=str(UserProfile.CreationMethod.EXTERNAL)).all()
   for user in users:
     _import_ldap_user(user.username)
+  return users
 
+def sync_ldap_groups():
+  """
+  Syncs LDAP group memberships. This will not import new
+  groups from LDAP. It is also not possible to import both a user and a
+  group at the same time. Each must be a separate operation. If neither a user,
+  nor a group is provided, all users and groups will be synced.
+  """
   groups = Group.objects.filter(group__in=LdapGroup.objects.all())
   for group in groups:
     _import_ldap_group(group.name)
+  return groups
 
 class GroupEditForm(forms.ModelForm):
   """
@@ -690,4 +744,8 @@ def _make_model_field(initial, choices, multi=True):
       field.initial = initial.pk
   return field
 
-
+class SyncLdapUsersGroupsForm(forms.Form):
+  ensure_home_directory = forms.BooleanField(label=_("Create Home Directories"),
+                                            help_text=_("Create home directory for every user, if one doesn't already exist."),
+                                            initial=True,
+                                            required=False)

+ 5 - 1
desktop/core/static/css/hue2.css

@@ -133,6 +133,10 @@ h1 {
     margin: 0;
 }
 
+.form-inline input, .form-inline a {
+  vertical-align: baseline;
+}
+
 .pagination p {
     padding-top:6px;
 }
@@ -945,4 +949,4 @@ div.box {
 .jHueNotify .close {
     right: -27px;
     display: none;
-}
+}