瀏覽代碼

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 年之前
父節點
當前提交
dd27176

+ 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;
-}
+}