瀏覽代碼

HUE-1071 [core] Add option to force password change on first login

Chris Conner 10 年之前
父節點
當前提交
3e20106

+ 12 - 0
apps/useradmin/src/useradmin/forms.py

@@ -139,6 +139,18 @@ class UserChangeForm(django.contrib.auth.forms.UserChangeForm):
     return user
 
 
+class PasswordChangeForm(UserChangeForm):
+  """
+  This inherits from UserChangeForm to allow for forced password change on first login
+  """
+  class Meta(UserChangeForm.Meta):
+    exclude = ('first_name', 'last_name', 'email')
+
+  def __init__(self, *args, **kwargs):
+    super(PasswordChangeForm, self).__init__(*args, **kwargs)
+    self.fields.pop('ensure_home_directory')
+
+
 class SuperUserChangeForm(UserChangeForm):
   class Meta(UserChangeForm.Meta):
     fields = ["username", "is_active"] + UserChangeForm.Meta.fields + ["is_superuser", "groups"]

+ 88 - 0
apps/useradmin/src/useradmin/migrations/0004_add_field_UserProfile_first_login.py

@@ -0,0 +1,88 @@
+# -*- coding: utf-8 -*-
+from south.utils import datetime_utils as datetime
+from south.db import db
+from south.v2 import SchemaMigration
+from django.db import models
+
+
+class Migration(SchemaMigration):
+
+    def forwards(self, orm):
+        # Adding field 'UserProfile.first_login'
+        db.add_column(u'useradmin_userprofile', 'first_login',
+                      self.gf('django.db.models.fields.BooleanField')(default=True),
+                      keep_default=False)
+
+
+    def backwards(self, orm):
+        # Deleting field 'UserProfile.first_login'
+        db.delete_column(u'useradmin_userprofile', 'first_login')
+
+
+    models = {
+        u'auth.group': {
+            'Meta': {'object_name': 'Group'},
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
+            'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
+        },
+        u'auth.permission': {
+            'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
+            'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
+        },
+        u'auth.user': {
+            'Meta': {'object_name': 'User'},
+            'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
+            'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
+            'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
+            'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
+            'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
+            'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
+            'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
+        },
+        u'contenttypes.contenttype': {
+            'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
+            'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
+        },
+        u'useradmin.grouppermission': {
+            'Meta': {'object_name': 'GroupPermission'},
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.Group']"}),
+            'hue_permission': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['useradmin.HuePermission']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+        },
+        u'useradmin.huepermission': {
+            'Meta': {'object_name': 'HuePermission'},
+            'action': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
+            'app': ('django.db.models.fields.CharField', [], {'max_length': '30'}),
+            'description': ('django.db.models.fields.CharField', [], {'max_length': '255'}),
+            'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Group']", 'through': u"orm['useradmin.GroupPermission']", 'symmetrical': 'False'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+        },
+        u'useradmin.ldapgroup': {
+            'Meta': {'object_name': 'LdapGroup'},
+            'group': ('django.db.models.fields.related.ForeignKey', [], {'related_name': "'group'", 'to': u"orm['auth.Group']"}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'})
+        },
+        u'useradmin.userprofile': {
+            'Meta': {'object_name': 'UserProfile'},
+            'creation_method': ('django.db.models.fields.CharField', [], {'default': "'HUE'", 'max_length': '64'}),
+            'first_login': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
+            'home_directory': ('django.db.models.fields.CharField', [], {'max_length': '1024', 'null': 'True'}),
+            u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
+            'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'unique': 'True'})
+        }
+    }
+
+    complete_apps = ['useradmin']

+ 2 - 0
apps/useradmin/src/useradmin/models.py

@@ -93,6 +93,8 @@ class UserProfile(models.Model):
   user = models.ForeignKey(auth_models.User, unique=True)
   home_directory = models.CharField(editable=True, max_length=1024, null=True)
   creation_method = models.CharField(editable=True, null=False, max_length=64, default=CreationMethod.HUE)
+  first_login = models.BooleanField(default=True, verbose_name=_t('First Login'),
+                                   help_text=_t('If this is users first login.'))
 
   def get_groups(self):
     return self.user.groups.all()

+ 131 - 0
apps/useradmin/src/useradmin/templates/change_password.mako

@@ -0,0 +1,131 @@
+## 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 desktop.views import commonheader, commonfooter
+from django.utils.translation import ugettext as _
+from useradmin.password_policy import is_password_policy_enabled, get_password_hint
+%>
+
+<%namespace name="layout" file="layout.mako" />
+
+${ commonheader(_('Hue Users'), "useradmin", user) | n,unicode }
+${ layout.menubar(section='users') }
+
+<div class="container-fluid">
+  <div class="card card-small">
+    <h1 class="card-heading simple">${_('Hue Users - Change password: %(username)s') % dict(username=username)}</h1>
+
+    <br/>
+    <form id="editForm" method="POST" class="form form-horizontal" autocomplete="off">
+    ${ csrf_token(request) | n,unicode }
+    <div id="properties" class="section">
+      <ul class="nav nav-tabs" style="margin-bottom: 0">
+        <li class="active">
+          <a href="#step1" class="step">${ _('Credentials') }
+          % if not username:
+            ${ _('(required)') }
+          % endif
+          </a>
+        </li>
+      </ul>
+
+    <div class="steps">
+      <div id="step1" class="stepDetails">
+        ${layout.render_field(form["username"], extra_attrs={'validate':'true'})}
+        % if "password1" in form.fields:
+          ${layout.render_field(form["password1"], extra_attrs=username is None and {'validate':'true'} or {})}
+          % if is_password_policy_enabled():
+            <div class="password_rule" style="margin-left:180px; width:500px;">
+              <p>${get_password_hint()}</p>
+            </div>
+          % endif
+          ${layout.render_field(form["password2"], extra_attrs=username is None and {'validate':'true'} or {})}
+          % if username and "password_old" in form.fields:
+            ${layout.render_field(form["password_old"], extra_attrs=username is None and {'validate':'true'} or {})}
+          % endif
+        % endif
+      </div>
+      <div class="form-actions">
+        <input type="submit" class="btn btn-primary" value="${_('Change password')}"/>
+      </div>
+    </form>
+  </div>
+</div>
+
+<script src="${ static('desktop/ext/js/routie-0.3.0.min.js') }" type="text/javascript" charset="utf-8"></script>
+
+<script type="text/javascript" charset="utf-8">
+
+$(document).ready(function(){
+ var currentStep = "step1";
+
+ routie({
+    "step1":function () {
+      showStep("step1");
+    }
+  });
+
+  function showStep(step) {
+    currentStep = step;
+    if (step != "step1") {
+      $("#backBtn").removeClass("disabled");
+    } else {
+      $("#backBtn").addClass("disabled");
+    }
+
+    if (step != $(".stepDetails:last").attr("id")) {
+      $("#nextBtn").removeClass("disabled");
+    } else {
+      $("#nextBtn").addClass("disabled");
+    }
+
+    $("a.step").parent().removeClass("active");
+    $("a.step[href=#" + step + "]").parent().addClass("active");
+    $(".stepDetails").hide();
+    $("#" + step).show();
+  }
+
+  function validateStep(step) {
+    var proceed = true;
+    $("#" + step).find("[validate=true]").each(function () {
+      if ($(this).val().trim() == "") {
+        proceed = false;
+        routie(step);
+        $(this).parents(".control-group").addClass("error");
+        $(this).parent().find(".help-inline").remove();
+        $(this).after("<span class=\"help-inline\"><strong>${ _('This field is required.') }</strong></span>");
+      }
+    });
+    return proceed;
+  }
+
+  $("[validate=true]").change(function () {
+    $(this).parents(".control-group").removeClass("error");
+    $(this).parent().find(".help-inline").remove();
+  });
+
+  $("#editForm").on("submit", function(){
+    if validateStep("step1") {
+      return true;
+    }
+    return false;
+  })
+});
+</script>
+
+${layout.commons()}
+
+${ commonfooter(messages) | n,unicode }

+ 27 - 7
apps/useradmin/src/useradmin/views.py

@@ -42,8 +42,8 @@ from hadoop.fs.exceptions import WebHdfsException
 from useradmin.models import HuePermission, UserProfile, LdapGroup
 from useradmin.models import get_profile, get_default_user_group
 from useradmin.forms import SyncLdapUsersGroupsForm, AddLdapGroupsForm, AddLdapUsersForm,\
-  PermissionsEditForm, GroupEditForm, SuperUserChangeForm, UserChangeForm, validate_username
-
+  PermissionsEditForm, GroupEditForm, SuperUserChangeForm, UserChangeForm, validate_username,\
+  PasswordChangeForm
 
 
 LOG = logging.getLogger(__name__)
@@ -170,6 +170,12 @@ def delete_group(request):
     return render("delete_group.mako", request, {'path': request.path})
 
 
+def require_change_password(self):
+  """ Return true if user has never logged in before. """
+  if desktop.conf.AUTH.BACKEND.get() == 'desktop.auth.backend.AllowFirstUserDjangoBackend' and self.first_login and desktop.conf.AUTH.CHANGE_DEFAULT_PASSWORD.get():
+    return True
+
+
 def edit_user(request, username=None):
   """
   edit_user(request, username = None) -> reply
@@ -182,12 +188,16 @@ def edit_user(request, username=None):
   if request.user.username != username and not request.user.is_superuser:
     raise PopupException(_("You must be a superuser to add or edit another user."), error_code=401)
 
+  userprofile = get_profile(request.user)
+
   if username is not None:
     instance = User.objects.get(username=username)
   else:
     instance = None
 
-  if request.user.is_superuser:
+  if require_change_password(userprofile):
+    form_class = PasswordChangeForm
+  elif request.user.is_superuser:
     form_class = SuperUserChangeForm
   else:
     form_class = UserChangeForm
@@ -225,12 +235,20 @@ def edit_user(request, username=None):
           __users_lock.release()
 
       # Ensure home directory is created, if necessary.
-      if form.cleaned_data['ensure_home_directory']:
+      if form.cleaned_data.get('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))
-      if request.user.is_superuser:
+
+      if require_change_password(userprofile):
+        userprofile.first_login = False
+        userprofile.save()
+        if request.user.is_superuser:
+          return redirect(reverse('about:index'))
+        else:
+          return redirect(reverse('desktop.views.home'))
+      elif request.user.is_superuser:
         return redirect(reverse(list_users))
       else:
         return redirect(reverse(edit_user, kwargs={'username': username}))
@@ -243,8 +261,10 @@ def edit_user(request, username=None):
     form = form_class(instance=instance, initial=initial)
     if request.user.is_superuser and request.user.username != username:
       form.fields.pop("password_old")
-
-  return render('edit_user.mako', request, dict(form=form, username=username))
+  if require_change_password(userprofile):
+    return render('change_password.mako', request, dict(form=form, username=username))
+  else:
+    return render('edit_user.mako', request, dict(form=form, username=username))
 
 
 def edit_group(request, name=None):

+ 3 - 0
desktop/conf.dist/hue.ini

@@ -231,6 +231,9 @@
     # Apply 'expires_after' to superusers.
     ## expire_superusers=true
 
+    # Force users to change password on first login with desktop.auth.backend.AllowFirstUserDjangoBackend
+    ## change_default_password=false
+
   # Configuration options for connecting to LDAP and Active Directory
   # -------------------------------------------------------------------
   [[ldap]]

+ 3 - 0
desktop/conf/pseudo-distributed.ini.tmpl

@@ -238,6 +238,9 @@
     # Apply 'expires_after' to superusers.
     ## expire_superusers=true
 
+    # Force users to change password on first login with desktop.auth.backend.AllowFirstUserDjangoBackend
+    ## change_default_password=false
+
   # Configuration options for connecting to LDAP and Active Directory
   # -------------------------------------------------------------------
   [[ldap]]

+ 3 - 0
desktop/core/src/desktop/auth/backend.py

@@ -171,6 +171,9 @@ class AllowFirstUserDjangoBackend(django.contrib.auth.backends.ModelBackend):
     if self.is_first_login_ever():
       user = find_or_create_user(username, password)
       user = rewrite_user(user)
+      userprofile = get_profile(user)
+      userprofile.first_login = False
+      userprofile.save()
 
       default_group = get_default_user_group()
       if default_group is not None:

+ 11 - 1
desktop/core/src/desktop/auth/views.py

@@ -35,7 +35,8 @@ from django.contrib.sessions.models import Session
 from django.http import HttpResponseRedirect
 from django.utils.translation import ugettext as _
 from hadoop.fs.exceptions import WebHdfsException
-from useradmin.views import ensure_home_directory
+from useradmin.models import get_profile
+from useradmin.views import ensure_home_directory, require_change_password
 
 from desktop.auth import forms as auth_forms
 from desktop.lib.django_util import render
@@ -106,7 +107,10 @@ def dt_login(request):
         # Must login by using the AuthenticationForm.
         # It provides 'backends' on the User object.
         user = auth_form.get_user()
+        userprofile = get_profile(user)
+
         login(request, user)
+
         if request.session.test_cookie_worked():
           request.session.delete_test_cookie()
 
@@ -118,6 +122,12 @@ def dt_login(request):
             LOG.error(_('Could not create home directory.'), exc_info=e)
             request.error(_('Could not create home directory.'))
 
+        if require_change_password(userprofile):
+          return HttpResponseRedirect(urlresolvers.reverse('useradmin.views.edit_user', kwargs={'username': user.username}))
+
+        userprofile.first_login = False
+        userprofile.save()
+
         access_warn(request, '"%s" login ok' % (user.username,))
         return HttpResponseRedirect(redirect_to)
 

+ 10 - 1
desktop/core/src/desktop/conf.py

@@ -524,7 +524,15 @@ AUTH = ConfigSection(
     EXPIRE_SUPERUSERS = Config("expire_superusers",
                                 help=_("Apply 'expires_after' to superusers."),
                                 type=coerce_bool,
-                                default=True)
+                                default=True),
+    CHANGE_DEFAULT_PASSWORD = Config(
+                            key="change_default_password",
+                            help=_("When set to true this will allow you to specify a password for "
+                                   "the user when you create the user and then force them to change "
+                                   "their password upon first login.  The default is false."),
+                            type=str,
+                            default="false",
+    )
 ))
 
 LDAP = ConfigSection(
@@ -772,6 +780,7 @@ OAUTH = ConfigSection(
       type=str,
       default="https://api.twitter.com/oauth/authorize"
     ),
+
   )
 )