Răsfoiți Sursa

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 ani în urmă
părinte
comite
dd27176c51

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

@@ -31,7 +31,7 @@ ${layout.menubar(section='users', _=_)}
             %if user.is_superuser == True:
             %if user.is_superuser == True:
             <a href="${ url('useradmin.views.edit_user') }" class="btn">${_('Add user')}</a>
             <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="${ 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
             %endif
         </div>
         </div>
         <form class="form-search">
         <form class="form-search">
@@ -80,25 +80,7 @@ ${layout.menubar(section='users', _=_)}
         </tbody>
         </tbody>
     </table>
     </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>
     <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
 import urllib
 
 
+from nose.plugins.attrib import attr
 from nose.tools import assert_true, assert_equal, assert_false
 from nose.tools import assert_true, assert_equal, assert_false
 
 
 from desktop.lib.django_test_util import make_logged_in_client
 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
 from useradmin.models import get_profile
 
 
 import useradmin.conf
 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
                   add_ldap_user, add_ldap_group, sync_ldap_users_groups
 import ldap_access
 import ldap_access
 
 
@@ -243,7 +245,8 @@ def test_user_admin():
 
 
   reset_all_users()
   reset_all_users()
   reset_all_groups()
   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.
   # Test basic output.
   response = c.get('/useradmin/')
   response = c.get('/useradmin/')
@@ -374,7 +377,8 @@ def test_useradmin_ldap_integration():
   assert_true(get_profile(larry).creation_method == str(UserProfile.CreationMethod.EXTERNAL))
   assert_true(get_profile(larry).creation_method == str(UserProfile.CreationMethod.EXTERNAL))
 
 
   # Should be a noop
   # 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(User.objects.all()), 1)
   assert_equal(len(Group.objects.all()), 0)
   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
   # Set up LDAP tests to use a LdapTestConnection instead of an actual LDAP connection
   ldap_access.CACHED_LDAP_CONN = LdapTestConnection()
   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.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
   # Set up LDAP tests to use a LdapTestConnection instead of an actual LDAP connection
   ldap_access.CACHED_LDAP_CONN = LdapTestConnection()
   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.get(URL))
   assert_true(c.post(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.forms.util import ErrorList
 from django.shortcuts import redirect
 from django.shortcuts import redirect
 
 
+from hadoop.fs.exceptions import WebHdfsException
 from useradmin.models import GroupPermission, HuePermission, UserProfile, LdapGroup
 from useradmin.models import GroupPermission, HuePermission, UserProfile, LdapGroup
 from useradmin.models import get_profile, get_default_user_group
 from useradmin.models import get_profile, get_default_user_group
 import ldap_access
 import ldap_access
@@ -44,7 +45,7 @@ __users_lock = threading.Lock()
 __groups_lock = threading.Lock()
 __groups_lock = threading.Lock()
 
 
 def list_users(request):
 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):
 def list_groups(request):
   return render("list_groups.mako", request, dict(groups=Group.objects.all()))
   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") })
       error_messages = {'invalid': _("Whitespaces and ':' not allowed") })
   password1 = forms.CharField(label=_("Password"), widget=forms.PasswordInput, required=False)
   password1 = forms.CharField(label=_("Password"), widget=forms.PasswordInput, required=False)
   password2 = forms.CharField(label=_("Password confirmation"), 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):
   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):
   def clean_password2(self):
     password1 = self.cleaned_data.get("password1", "")
     password1 = self.cleaned_data.get("password1", "")
@@ -186,7 +191,7 @@ def edit_user(request, username=None):
     form = form_class(request.POST, instance=instance)
     form = form_class(request.POST, instance=instance)
     if form.is_valid(): # All validation rules pass
     if form.is_valid(): # All validation rules pass
       if instance is None:
       if instance is None:
-        form.save()
+        instance = form.save()
       else:
       else:
         #
         #
         # Check for 3 more conditions:
         # Check for 3 more conditions:
@@ -215,9 +220,15 @@ def edit_user(request, username=None):
         finally:
         finally:
           __users_lock.release()
           __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))
       return redirect(reverse(list_users))
   else:
   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))
   return render('edit_user.mako', request, dict(form=form, action=request.path, username=username))
 
 
 def edit_group(request, name=None):
 def edit_group(request, name=None):
@@ -292,6 +303,10 @@ class AddLdapUserForm(forms.Form):
                                     "distinguished name."),
                                     "distinguished name."),
                           initial=False,
                           initial=False,
                           required=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):
   def clean(self):
     cleaned_data = super(AddLdapUserForm, self).clean()
     cleaned_data = super(AddLdapUserForm, self).clean()
@@ -325,6 +340,11 @@ def add_ldap_user(request):
       username = form.cleaned_data['username']
       username = form.cleaned_data['username']
       import_by_dn = form.cleaned_data['dn']
       import_by_dn = form.cleaned_data['dn']
       user = import_ldap_user(username, import_by_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:
       if user is None:
         errors = form._errors.setdefault('username', ErrorList())
         errors = form._errors.setdefault('username', ErrorList())
@@ -352,6 +372,10 @@ class AddLdapGroupForm(forms.Form):
                                       help_text=_('Import unimported or new users from the group.'),
                                       help_text=_('Import unimported or new users from the group.'),
                                       initial=False,
                                       initial=False,
                                       required=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):
   def clean(self):
     cleaned_data = super(AddLdapGroupForm, self).clean()
     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."))
     raise PopupException(_("You must be a superuser to sync the LDAP users/groups."))
 
 
   if request.method == 'POST':
   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):
 def _check_remove_last_super(user_obj):
   """Raise an error if we're removing the last superuser"""
   """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):
 def import_ldap_group(group, import_members, import_by_dn):
   return _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,
   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.
   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()
   users = User.objects.filter(userprofile__creation_method=str(UserProfile.CreationMethod.EXTERNAL)).all()
   for user in users:
   for user in users:
     _import_ldap_user(user.username)
     _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())
   groups = Group.objects.filter(group__in=LdapGroup.objects.all())
   for group in groups:
   for group in groups:
     _import_ldap_group(group.name)
     _import_ldap_group(group.name)
+  return groups
 
 
 class GroupEditForm(forms.ModelForm):
 class GroupEditForm(forms.ModelForm):
   """
   """
@@ -690,4 +744,8 @@ def _make_model_field(initial, choices, multi=True):
       field.initial = initial.pk
       field.initial = initial.pk
   return field
   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;
     margin: 0;
 }
 }
 
 
+.form-inline input, .form-inline a {
+  vertical-align: baseline;
+}
+
 .pagination p {
 .pagination p {
     padding-top:6px;
     padding-top:6px;
 }
 }
@@ -945,4 +949,4 @@ div.box {
 .jHueNotify .close {
 .jHueNotify .close {
     right: -27px;
     right: -27px;
     display: none;
     display: none;
-}
+}