浏览代码

HUE-668. [admin] Enable the LDAP commands to be performed through the UI

Add a 'Add Ldap user' button to the admin homepage
Add a 'Add Ldap group' button to the group page
Add a 'Sync all' users and groups button to the admin homepage
Romain Rigaux 13 年之前
父节点
当前提交
c2ace762c1

+ 134 - 111
apps/useradmin/src/useradmin/templates/list_groups.mako

@@ -26,26 +26,27 @@ ${commonheader("Hue Groups", "useradmin", "100px")}
 ${layout.menubar(section='groups')}
 
 <div class="container-fluid">
-	<h1>Hue Groups</h1>
-	<div class="well hueWell">
-		<p class="pull-right">
-			%if user.is_superuser == True:
-			<a id="addGroupBtn" href="#" class="btn">Add group</a>
-			%endif
-		</p>
-		<form class="form-search">
-			Filter: <input id="filterInput" class="input-xlarge search-query" placeholder="Search for group name, members, etc...">
-		</form>
-	</div>
+    <h1>Hue Groups</h1>
+    <div class="well hueWell">
+        <div class="pull-right btn-group">
+            %if user.is_superuser == True:
+            <a id="addGroupBtn" href="#" class="btn">Add group</a>
+            <a id="addLdapGroupBtn" href="#" class="btn">Add/Sync LDAP group</a>
+            %endif
+        </div>
+        <form class="form-search">
+            Filter: <input id="filterInput" class="input-xlarge search-query" placeholder="Search for group name, members, etc...">
+        </form>
+    </div>
       <table class="table table-striped datatables">
         <thead>
           <tr>
             <th>${_('Group Name')}</th>
             <th>${_('Members')}</th>
             <th>${_('Permissions')}</th>
-			%if user.is_superuser == True:
-			<th>&nbsp;</th>
-			%endif
+            %if user.is_superuser == True:
+            <th>&nbsp;</th>
+            %endif
           </tr>
         </head>
         <tbody>
@@ -54,12 +55,12 @@ ${layout.menubar(section='groups')}
             <td>${group.name}</td>
             <td>${', '.join([group_user.username for group_user in group.user_set.all()])}</td>
             <td>${', '.join([perm.app + "." + perm.action for perm in group_permissions(group)])}</td>
-			%if user.is_superuser == True:
+            %if user.is_superuser == True:
             <td>
               <a title="Edit ${group.name}" class="btn small editGroupBtn" data-url="${ url('useradmin.views.edit_group', name=urllib.quote(group.name)) }" data-name="${group.name}">Edit</a>
               <a title="Delete ${group.name}" class="btn small confirmationModal" alt="Are you sure you want to delete ${group.name}?" href="javascript:void(0)" data-confirmation-url="${ url('useradmin.views.delete_group', name=urllib.quote_plus(group.name)) }">Delete</a>
             </td>
-			%endif
+            %endif
           </tr>
         % endfor
         </tbody>
@@ -68,110 +69,132 @@ ${layout.menubar(section='groups')}
 
 
 <div id="deleteGroup" class="modal hide fade groupModal">
-	<form id="deleteGroupForm" action="" method="POST">
-	<div class="modal-header">
-		<a href="#" class="close" data-dismiss="modal">&times;</a>
-		<h3 id="deleteGroupMessage">Confirm action</h3>
-	</div>
-	<div class="modal-footer">
-		<input type="submit" class="btn primary" value="Yes"/>
-		<a href="#" class="btn secondary hideModal">No</a>
-	</div>
-	</form>
+    <form id="deleteGroupForm" action="" method="POST">
+    <div class="modal-header">
+        <a href="#" class="close" data-dismiss="modal">&times;</a>
+        <h3 id="deleteGroupMessage">Confirm action</h3>
+    </div>
+    <div class="modal-footer">
+        <input type="submit" class="btn primary" value="Yes"/>
+        <a href="#" class="btn secondary hideModal">No</a>
+    </div>
+    </form>
 </div>
 
 <div id="addGroup" class="modal hide fade groupModal">
-	<div class="modal-header">
-		<a href="#" class="close" data-dismiss="modal">&times;</a>
-		<h3>Add group</h3>
-	</div>
-	<div id="addGroupBody" class="modal-body">
-		<iframe id="addGroupFrame" frameBorder="0"></iframe>
-	</div>
-	<div class="modal-footer">
-		<button id="addGroupSaveBtn" class="btn primary">Save</button>
-	</div>
+    <div class="modal-header">
+        <a href="#" class="close" data-dismiss="modal">&times;</a>
+        <h3>Add group</h3>
+    </div>
+    <div id="addGroupBody" class="modal-body">
+        <iframe id="addGroupFrame" frameBorder="0"></iframe>
+    </div>
+    <div class="modal-footer">
+        <button id="addGroupSaveBtn" class="btn primary">Save</button>
+    </div>
+</div>
+
+<div id="addLdapGroup" class="modal hide fade groupModal">
+    <div class="modal-header">
+        <a href="#" class="close" data-dismiss="modal">&times;</a>
+        <h3>Add or Sync a LDAP group</h3>
+    </div>
+    <div id="addLdapGroupBody" class="modal-body">
+        <iframe id="addLdapGroupFrame" frameBorder="0"></iframe>
+    </div>
+    <div class="modal-footer">
+        <button id="addLdapGroupSaveBtn" class="btn primary">Save</button>
+    </div>
 </div>
 
 <div id="editGroup" class="modal hide fade groupModal">
-	<div class="modal-header">
-		<a href="#" class="close" data-dismiss="modal">&times;</a>
-		<h3>Edit group <span class="groupName"></span></h3>
-	</div>
-	<div id="editGroupBody" class="modal-body">
-		<iframe id="editGroupFrame" frameBorder="0"></iframe>
-	</div>
-	<div class="modal-footer">
-		<button id="editGroupSaveBtn" class="btn primary">Save</button>
-	</div>
+    <div class="modal-header">
+        <a href="#" class="close" data-dismiss="modal">&times;</a>
+        <h3>Edit group <span class="groupName"></span></h3>
+    </div>
+    <div id="editGroupBody" class="modal-body">
+        <iframe id="editGroupFrame" frameBorder="0"></iframe>
+    </div>
+    <div class="modal-footer">
+        <button id="editGroupSaveBtn" class="btn primary">Save</button>
+    </div>
 </div>
 
 </div>
 
-	<script type="text/javascript" charset="utf-8">
-		$(document).ready(function(){
-			$(".datatables").dataTable({
-				"bPaginate": false,
-			    "bLengthChange": false,
-				"bInfo": false,
-				"bFilter": false,
-				"aoColumns": [
-					{ "sWidth": "20%" },
-					{ "sWidth": "20%" },
-					null,
-					%if user.is_superuser == True:
-					{ "sWidth": "120px" },
-					%endif
-				 ]
-			});
-			$(".dataTables_wrapper").css("min-height","0");
-			$(".dataTables_filter").hide();
-
-			$(".confirmationModal").click(function(){
-				var _this = $(this);
-				$.getJSON(_this.attr("data-confirmation-url"), function(data){
-					$("#deleteGroupForm").attr("action", data.path);
-					$("#deleteGroupMessage").text(_this.attr("alt"));
-				});
-				$("#deleteGroup").modal("show");
-			});
-			$(".hideModal").click(function(){
-				$("#deleteGroup").modal("hide");
-			});
-
-			$("#filterInput").keyup(function(){
-		        $.each($(".groupRow"), function(index, value) {
-
-		          if($(value).data("search").toLowerCase().indexOf($("#filterInput").val().toLowerCase()) == -1 && $("#filterInput").val() != ""){
-		            $(value).hide(250);
-		          }else{
-		            $(value).show(250);
-		          }
-		        });
-
-		    });
-
-			$("#addGroupBtn").click(function(){
-				$("#addGroupFrame").css("height","400px").attr("src","${url('useradmin.views.edit_group')}");
-				$("#addGroup").modal("show");
-			});
-
-			$("#addGroupSaveBtn").click(function(){
-				$("#addGroupFrame").contents().find('form').submit();
-			});
-
-			$(".editGroupBtn").click(function(){
-				$("#editGroup").find(".groupName").text($(this).data("name"));
-				$("#editGroupFrame").css("height","400px").attr("src", $(this).data("url"));
-				$("#editGroup").modal("show");
-			});
-
-			$("#editGroupSaveBtn").click(function(){
-				$("#editGroupFrame").contents().find('form').submit();
-			});
-
-
-		});
-	</script>
+    <script type="text/javascript" charset="utf-8">
+        $(document).ready(function(){
+            $(".datatables").dataTable({
+                "bPaginate": false,
+                "bLengthChange": false,
+                "bInfo": false,
+                "bFilter": false,
+                "aoColumns": [
+                    { "sWidth": "20%" },
+                    { "sWidth": "20%" },
+                    null,
+                    %if user.is_superuser == True:
+                    { "sWidth": "120px" },
+                    %endif
+                 ]
+            });
+            $(".dataTables_wrapper").css("min-height","0");
+            $(".dataTables_filter").hide();
+
+            $(".confirmationModal").click(function(){
+                var _this = $(this);
+                $.getJSON(_this.attr("data-confirmation-url"), function(data){
+                    $("#deleteGroupForm").attr("action", data.path);
+                    $("#deleteGroupMessage").text(_this.attr("alt"));
+                });
+                $("#deleteGroup").modal("show");
+            });
+            $(".hideModal").click(function(){
+                $("#deleteGroup").modal("hide");
+            });
+
+            $("#filterInput").keyup(function(){
+                $.each($(".groupRow"), function(index, value) {
+
+                  if($(value).data("search").toLowerCase().indexOf($("#filterInput").val().toLowerCase()) == -1 && $("#filterInput").val() != ""){
+                    $(value).hide(250);
+                  }else{
+                    $(value).show(250);
+                  }
+                });
+
+            });
+
+            $("#addGroupBtn").click(function(){
+                $("#addGroupFrame").css("height","400px").attr("src","${url('useradmin.views.edit_group')}");
+                $("#addGroup").modal("show");
+            });
+
+            $("#addGroupSaveBtn").click(function(){
+                $("#addGroupFrame").contents().find('form').submit();
+            });
+
+            $("#addLdapGroupBtn").click(function(){
+                $("#addLdapGroupFrame").css("height","200px").attr("src","${url('useradmin.views.add_ldap_group')}");
+                $("#addLdapGroup").modal("show");
+            });
+
+            $("#addLdapGroupSaveBtn").click(function(){
+                $("#addLdapGroupFrame").contents().find('form').submit();
+            });
+
+            $(".editGroupBtn").click(function(){
+                $("#editGroup").find(".groupName").text($(this).data("name"));
+                $("#editGroupFrame").css("height","400px").attr("src", $(this).data("url"));
+                $("#editGroup").modal("show");
+            });
+
+            $("#editGroupSaveBtn").click(function(){
+                $("#editGroupFrame").contents().find('form').submit();
+            });
+
+
+        });
+    </script>
 
 ${commonfooter()}

+ 194 - 148
apps/useradmin/src/useradmin/templates/list_users.mako

@@ -26,158 +26,204 @@ ${commonheader("Hue Users", "useradmin", "100px")}
 ${layout.menubar(section='users')}
 
 <div class="container-fluid">
-	<h1>Hue Users</h1>
-	<div class="well hueWell">
-		<p class="pull-right">
-			%if user.is_superuser == True:
-			<a id="addUserBtn" href="#" class="btn">Add user</a>
-			%endif
-		</p>
-		<form class="form-search">
-			Filter: <input id="filterInput" class="input-xlarge search-query" placeholder="Search for username, name, e-mail, etc...">
-		</form>
-	</div>
-	<table class="table table-striped datatables">
-		<thead>
-			<tr>
-            	<th>${_('Username')}</th>
-	            <th>${_('First Name')}</th>
-	            <th>${_('Last Name')}</th>
-	            <th>${_('E-mail')}</th>
-				<th>${_('Groups')}</th>
-	            <th>${_('Last Login')}</th>
-				<th>&nbsp;</th>
-			</tr>
-		</thead>
-		<tbody>
+    <h1>Hue Users</h1>
+    <div class="well hueWell">
+        <div class="btn-group pull-right">
+            %if user.is_superuser == True:
+            <a id="addUserBtn" href="#" class="btn">Add user</a>
+            <a id="addLdapUserBtn" href="#" class="btn">Add/sync LDAP user</a>
+            <a id="syncLdapBtn" href="#" class="btn">Sync LDAP users/groups</a>
+            %endif
+        </div>
+        <form class="form-search">
+            Filter: <input id="filterInput" class="input-xlarge search-query" placeholder="Search for username, name, e-mail, etc...">
+        </form>
+    </div>
+    <table class="table table-striped datatables">
+        <thead>
+            <tr>
+                <th>${_('Username')}</th>
+                <th>${_('First Name')}</th>
+                <th>${_('Last Name')}</th>
+                <th>${_('E-mail')}</th>
+                <th>${_('Groups')}</th>
+                <th>${_('Last Login')}</th>
+                <th>&nbsp;</th>
+            </tr>
+        </thead>
+        <tbody>
         % for listed_user in users:
-			<tr class="userRow" data-search="${listed_user.username}${listed_user.first_name}${listed_user.last_name}${listed_user.email}${', '.join([group.name for group in listed_user.groups.all()])}">
-            	<td>${listed_user.username}</td>
-	            <td>${listed_user.first_name}</td>
-	            <td>${listed_user.last_name}</td>
-	            <td>${listed_user.email}</td>
-				<td>${', '.join([group.name for group in listed_user.groups.all()])}</td>
-	            <td>${date(listed_user.last_login)} ${time(listed_user.last_login).replace("p.m.","PM").replace("a.m.","AM")}</td>
-	            <td class="right">
-				%if user.is_superuser == True:
-					<a title="Edit ${listed_user.username}" class="btn small editUserBtn" data-url="${ url('useradmin.views.edit_user', username=urllib.quote(listed_user.username)) }" data-name="${listed_user.username}">Edit</a>
-					<a title="Delete ${listed_user.username}" class="btn small confirmationModal" alt="Are you sure you want to delete ${listed_user.username}?" href="javascript:void(0)" data-confirmation-url="${ url('useradmin.views.delete_user', username=urllib.quote_plus(listed_user.username)) }">Delete</a>
-				%else:
-					%if user.username == listed_user.username:
-						<a title="Edit ${listed_user.username}" class="btn small editUserBtn" data-url="${ url('useradmin.views.edit_user', username=urllib.quote(listed_user.username)) }" data-name="${listed_user.username}">Edit</a>
-					%else:
-						&nbsp;
-					%endif
-				%endif
-				</td>
-			</tr>
-		% endfor
-		</tbody>
-	</table>
-
-	<div id="addUser" class="modal hide fade userModal">
-		<div class="modal-header">
-			<a href="#" class="close" data-dismiss="modal">&times;</a>
-			<h3>Add user</h3>
-		</div>
-		<div id="addUserBody" class="modal-body">
-			<iframe id="addUserFrame" class="scroll" frameBorder="0"></iframe>
-		</div>
-		<div class="modal-footer">
-			<button id="addUserSaveBtn" class="btn primary">Save</button>
-		</div>
-	</div>
-
-	<div id="editUser" class="modal hide fade userModal">
-		<div class="modal-header">
-			<a href="#" class="close" data-dismiss="modal">&times;</a>
-			<h3>Edit user <span class="username"></span></h3>
-		</div>
-		<div id="editUserBody" class="modal-body">
-			<iframe id="editUserFrame" class="scroll" frameBorder="0"></iframe>
-		</div>
-		<div class="modal-footer">
-			<button id="editUserSaveBtn" class="btn primary">Save</button>
-		</div>
-	</div>
-
-	<div id="deleteUser" class="modal hide fade userModal">
-		<form id="deleteUserForm" action="" method="POST">
-		<div class="modal-header">
-			<a href="#" class="close" data-dismiss="modal">&times;</a>
-			<h3 id="deleteUserMessage">Confirm action</h3>
-		</div>
-		<div class="modal-footer">
-			<input type="submit" class="btn primary" value="Yes"/>
-			<a href="#" class="btn secondary" data-dismiss="modal">No</a>
-		</div>
-		</form>
-	</div>
+            <tr class="userRow" data-search="${listed_user.username}${listed_user.first_name}${listed_user.last_name}${listed_user.email}${', '.join([group.name for group in listed_user.groups.all()])}">
+                <td>${listed_user.username}</td>
+                <td>${listed_user.first_name}</td>
+                <td>${listed_user.last_name}</td>
+                <td>${listed_user.email}</td>
+                <td>${', '.join([group.name for group in listed_user.groups.all()])}</td>
+                <td>${date(listed_user.last_login)} ${time(listed_user.last_login).replace("p.m.","PM").replace("a.m.","AM")}</td>
+                <td class="right">
+                %if user.is_superuser == True:
+                    <a title="Edit ${listed_user.username}" class="btn small editUserBtn" data-url="${ url('useradmin.views.edit_user', username=urllib.quote(listed_user.username)) }" data-name="${listed_user.username}">Edit</a>
+                    <a title="Delete ${listed_user.username}" class="btn small confirmationModal" alt="Are you sure you want to delete ${listed_user.username}?" href="javascript:void(0)" data-confirmation-url="${ url('useradmin.views.delete_user', username=urllib.quote_plus(listed_user.username)) }">Delete</a>
+                %else:
+                    %if user.username == listed_user.username:
+                        <a title="Edit ${listed_user.username}" class="btn small editUserBtn" data-url="${ url('useradmin.views.edit_user', username=urllib.quote(listed_user.username)) }" data-name="${listed_user.username}">Edit</a>
+                    %else:
+                        &nbsp;
+                    %endif
+                %endif
+                </td>
+            </tr>
+        % endfor
+        </tbody>
+    </table>
+
+    <div id="addUser" class="modal hide fade userModal">
+        <div class="modal-header">
+            <a href="#" class="close" data-dismiss="modal">&times;</a>
+            <h3>Add user</h3>
+        </div>
+        <div id="addUserBody" class="modal-body">
+            <iframe id="addUserFrame" class="scroll" frameBorder="0"></iframe>
+        </div>
+        <div class="modal-footer">
+            <button id="addUserSaveBtn" class="btn primary">Save</button>
+        </div>
+    </div>
+
+    <div id="addLdapUser" class="modal hide fade userModal">
+        <div class="modal-header">
+            <a href="#" class="close" data-dismiss="modal">&times;</a>
+            <h3>Add or Sync a LDAP user</h3>
+        </div>
+        <div id="addLdapUserBody" class="modal-body">
+            <iframe id="addLdapUserFrame" class="scroll" frameBorder="0"></iframe>
+        </div>
+        <div class="modal-footer">
+            <button id="addLdapUserSaveBtn" class="btn primary">Save</button>
+        </div>
+    </div>
+
+    <div id="syncLdap" class="modal hide fade userModal">
+        <div class="modal-header">
+            <a href="#" class="close" data-dismiss="modal">&times;</a>
+            <h3>Sync LDAP users and groups</h3>
+        </div>
+        <div id="syncLdapBody" class="modal-body">
+            <iframe id="syncLdapFrame" class="scroll" frameBorder="0"></iframe>
+        </div>
+        <div class="modal-footer">
+            <button id="syncLdapSaveBtn" class="btn primary">Sync</button>
+        </div>
+    </div>
+
+    <div id="editUser" class="modal hide fade userModal">
+        <div class="modal-header">
+            <a href="#" class="close" data-dismiss="modal">&times;</a>
+            <h3>Edit user <span class="username"></span></h3>
+        </div>
+        <div id="editUserBody" class="modal-body">
+            <iframe id="editUserFrame" class="scroll" frameBorder="0"></iframe>
+        </div>
+        <div class="modal-footer">
+            <button id="editUserSaveBtn" class="btn primary">Save</button>
+        </div>
+    </div>
+
+    <div id="deleteUser" class="modal hide fade userModal">
+        <form id="deleteUserForm" action="" method="POST">
+        <div class="modal-header">
+            <a href="#" class="close" data-dismiss="modal">&times;</a>
+            <h3 id="deleteUserMessage">Confirm action</h3>
+        </div>
+        <div class="modal-footer">
+            <input type="submit" class="btn primary" value="Yes"/>
+            <a href="#" class="btn secondary" data-dismiss="modal">No</a>
+        </div>
+        </form>
+    </div>
 
 
 </div>
 
-	<script type="text/javascript" charset="utf-8">
-		$(document).ready(function(){
-			$(".datatables").dataTable({
-				"bPaginate": false,
-			    "bLengthChange": false,
-				"bInfo": false,
-				"bFilter": false,
-				"aoColumns": [
-						null,
-						null,
-						null,
-						null,
-						null,
-						null,
-						{ "sType": "date" }
-					]
-			});
-			$(".dataTables_wrapper").css("min-height","0");
-			$(".dataTables_filter").hide();
-
-			$(".confirmationModal").click(function(){
-				var _this = $(this);
-				$.getJSON(_this.attr("data-confirmation-url"), function(data){
-					$("#deleteUserForm").attr("action", data.path);
-					$("#deleteUserMessage").text(_this.attr("alt"));
-				});
-				$("#deleteUser").modal("show");
-			});
-
-			$("#filterInput").keyup(function(){
-		        $.each($(".userRow"), function(index, value) {
-		          if($(value).data("search").toLowerCase().indexOf($("#filterInput").val().toLowerCase()) == -1 && $("#filterInput").val() != ""){
-		            $(value).hide(250);
-		          }else{
-		            $(value).show(250);
-		          }
-		        });
-
-		    });
-
-			$("#addUserBtn").click(function(){
-				$("#addUserFrame").css("height","300px").attr("src","${ url('useradmin.views.edit_user') }");
-				$("#addUser").modal("show");
-			});
-
-			$("#addUserSaveBtn").click(function(){
-				$("#addUserFrame").contents().find('form').submit();
-			});
-
-			$(".editUserBtn").click(function(){
-				$("#editUser").find(".username").text($(this).data("name"));
-				$("#editUserFrame").css("height","300px").attr("src", $(this).data("url"));
-				$("#editUser").modal("show");
-			});
-
-			$("#editUserSaveBtn").click(function(){
-				$("#editUserFrame").contents().find('form').submit();
-			});
-
-
-		});
-	</script>
+    <script type="text/javascript" charset="utf-8">
+        $(document).ready(function(){
+            $(".datatables").dataTable({
+                "bPaginate": false,
+                "bLengthChange": false,
+                "bInfo": false,
+                "bFilter": false,
+                "aoColumns": [
+                        null,
+                        null,
+                        null,
+                        null,
+                        null,
+                        null,
+                        { "sType": "date" }
+                    ]
+            });
+            $(".dataTables_wrapper").css("min-height","0");
+            $(".dataTables_filter").hide();
+
+            $(".confirmationModal").click(function(){
+                var _this = $(this);
+                $.getJSON(_this.attr("data-confirmation-url"), function(data){
+                    $("#deleteUserForm").attr("action", data.path);
+                    $("#deleteUserMessage").text(_this.attr("alt"));
+                });
+                $("#deleteUser").modal("show");
+            });
+
+            $("#filterInput").keyup(function(){
+                $.each($(".userRow"), function(index, value) {
+                  if($(value).data("search").toLowerCase().indexOf($("#filterInput").val().toLowerCase()) == -1 && $("#filterInput").val() != ""){
+                    $(value).hide(250);
+                  }else{
+                    $(value).show(250);
+                  }
+                });
+
+            });
+
+            $("#addUserBtn").click(function(){
+                $("#addUserFrame").css("height","300px").attr("src","${ url('useradmin.views.edit_user') }");
+                $("#addUser").modal("show");
+            });
+
+            $("#addUserSaveBtn").click(function(){
+                $("#addUserFrame").contents().find('form').submit();
+            });
+
+            $("#addLdapUserBtn").click(function(){
+                $("#addLdapUserFrame").css("height","150px").attr("src","${ url('useradmin.views.add_ldap_user') }");
+                $("#addLdapUser").modal("show");
+            });
+
+            $("#addLdapUserSaveBtn").click(function(){
+                $("#addLdapUserFrame").contents().find('form').submit();
+            });
+
+            $("#syncLdapBtn").click(function(){
+                $("#syncLdapFrame").css("height","150px").attr("src","${ url('useradmin.views.sync_ldap_users_groups') }");
+                $("#syncLdap").modal("show");
+            });
+
+            $("#syncLdapSaveBtn").click(function(){
+                $("#syncLdapFrame").contents().find('form').submit();
+            });
+
+            $(".editUserBtn").click(function(){
+                $("#editUser").find(".username").text($(this).data("name"));
+                $("#editUserFrame").css("height","300px").attr("src", $(this).data("url"));
+                $("#editUser").modal("show");
+            });
+
+            $("#editUserSaveBtn").click(function(){
+                $("#editUserFrame").contents().find('form').submit();
+            });
+
+
+        });
+    </script>
 
 ${commonfooter()}

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

@@ -0,0 +1,37 @@
+## 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_iframe, commonfooter_iframe
+import urllib
+%>
+
+${commonheader_iframe()}
+<form id="editForm" class="form-horizontal" action="${urllib.quote(action)}" method="POST">
+    <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>
+</form>
+
+<script type="text/javascript" charset="utf-8">
+	$(document).ready(function(){
+		$("#id_groups").jHueSelector();
+	});
+</script>
+${commonfooter_iframe()}

+ 64 - 3
apps/useradmin/src/useradmin/tests.py

@@ -28,12 +28,14 @@ from nose.tools import assert_true, assert_equal, assert_false
 from desktop.lib.django_test_util import make_logged_in_client
 from django.contrib.auth.models import User, Group
 from django.utils.encoding import smart_unicode
+from django.core.urlresolvers import reverse
 
 from useradmin.models import HuePermission, GroupPermission, LdapGroup, UserProfile
 from useradmin.models import get_profile
 
 import useradmin.conf
-from views import sync_ldap_users_and_groups, import_ldap_user, import_ldap_group
+from views import sync_ldap_users_and_groups, import_ldap_user, import_ldap_group, \
+                  add_ldap_user, add_ldap_group, sync_ldap_users_groups
 import ldap_access
 
 def reset_all_users():
@@ -68,11 +70,11 @@ class LdapTestConnection(object):
 
   def find_user(self, user, find_by_dn=False):
     """ Returns info for a particular user """
-    return LdapTestConnection._instance.users[user]
+    return LdapTestConnection._instance.users.get(user, None)
 
   def find_group(self, groupname, find_by_dn=False):
     """ Return all groups in the system with parents and children """
-    return LdapTestConnection._instance.groups[groupname]
+    return LdapTestConnection._instance.groups.get(groupname, None)
 
   class _Singleton:
     def __init__(self):
@@ -411,3 +413,62 @@ def test_useradmin_ldap_integration():
   import_ldap_group('OtherGroup', import_members=False, import_by_dn=False)
   assert_false(LdapGroup.objects.filter(group=hue_group).exists())
   assert_true(hue_group.user_set.filter(username=hue_user.username).exists())
+
+def test_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()
+
+
+  c = make_logged_in_client(username='test', is_superuser=True)
+
+  assert_true(c.get(URL))
+
+  response = c.post(URL, dict(username='moe', password1='test', password2='test'))
+  assert_equal(response.context['action'], '/useradmin/users')
+
+  response = c.post(URL, dict(username='bad_name', password1='test', password2='test'))
+  assert_true('Could not' in response.context['form'].errors['username'][0])
+
+def test_add_ldap_group():
+  URL = reverse(add_ldap_group)
+
+  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()
+
+
+  c = make_logged_in_client(username='test', is_superuser=True)
+
+  assert_true(c.get(URL))
+
+  response = c.post(URL, dict(name='TestUsers', password1='test', password2='test'))
+  assert_equal(response.context['action'], '/useradmin/groups')
+
+  response = c.post(URL, dict(name='bad_name', password1='test', password2='test'))
+  assert_true('Could not' in response.context['form'].errors['name'][0])
+
+  response = c.post(URL, dict(name='toolongnametoolongnametoolongname',
+                              password1='test', password2='test'))
+  assert_true('Too long' in response.context['form'].errors['name'][0])
+
+def test_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()
+
+
+  c = make_logged_in_client(username='test', is_superuser=True)
+
+  assert_true(c.get(URL))
+  assert_true(c.post(URL))

+ 3 - 0
apps/useradmin/src/useradmin/urls.py

@@ -27,6 +27,9 @@ urlpatterns = patterns('useradmin',
   url(r'^groups$', 'views.list_groups'),
   url(r'^permissions$', 'views.list_permissions'),
   url(r'^users/edit/(?P<username>%s)$' % (username_re,), 'views.edit_user'),
+  url(r'^users/add_ldap_user$', 'views.add_ldap_user'),
+  url(r'^users/add_ldap_group$', 'views.add_ldap_group'),
+  url(r'^users/sync_ldap_users_groups$', 'views.sync_ldap_users_groups'),
   url(r'^groups/edit/(?P<name>%s)$' % (groupname_re,), 'views.edit_group'),
   url(r'^permissions/edit/(?P<app>.*)/(?P<priv>.*)$', 'views.edit_permission'),
   url(r'^users/new$', 'views.edit_user', name="useradmin.new"),

+ 138 - 2
apps/useradmin/src/useradmin/views.py

@@ -30,6 +30,7 @@ from django import forms
 from django.contrib.auth.models import User, Group
 from desktop.lib.django_util import get_username_re_rule, get_groupname_re_rule, render, PopupException
 from django.core import urlresolvers
+from django.forms.util import ErrorList
 
 from useradmin.models import GroupPermission, HuePermission, UserProfile, LdapGroup
 from useradmin.models import get_profile, get_default_user_group
@@ -286,6 +287,141 @@ def edit_permission(request, app=None, priv=None):
   return render('edit_permissions.mako', request,
     dict(form=form, action=request.path, app=app, priv=priv))
 
+class AddLdapUserForm(forms.Form):
+  username = forms.RegexField(
+      label="Username",
+      max_length=64,
+      regex='^%s$' % (get_username_re_rule(),),
+      help_text="Required. 30 characters or fewer. No whitespaces or colons.",
+      error_messages={'invalid': "Whitespaces and ':' not allowed" })
+  dn = forms.BooleanField(label="Distinguished name",
+                          help_text="Whether or not the user should be imported by "
+                                    "distinguished name",
+                          initial=False,
+                          required=False)
+
+  def clean(self):
+    cleaned_data = super(AddLdapUserForm, self).clean()
+    username = cleaned_data.get("username")
+    dn = cleaned_data.get("dn")
+
+    if not dn:
+      if username is not None and len(username) > 30:
+        msg = 'Too long: 30 characters or fewer and not %s' % (len(username),)
+        errors = self._errors.setdefault('username', ErrorList())
+        errors.append(msg)
+        raise forms.ValidationError(msg)
+
+    return cleaned_data
+
+def add_ldap_user(request):
+  """
+  add_ldap_user(request) -> reply
+
+  Handler for importing LDAP users into the Hue database.
+
+  If a user has been previously imported, this will sync their user information.
+  If the LDAP request failed, the error message is generic right now.
+  """
+  if not request.user.is_superuser:
+    raise PopupException("You must be a superuser to add another user.")
+
+  if request.method == 'POST':
+    form = AddLdapUserForm(request.POST)
+    if form.is_valid():
+      username = form.cleaned_data['username']
+      import_by_dn = form.cleaned_data['dn']
+      user = import_ldap_user(username, import_by_dn)
+
+      if user is None:
+        errors = form._errors.setdefault('username', ErrorList())
+        errors.append('Could not get LDAP details for user %s' % (username,))
+      else:
+        request.path = urlresolvers.reverse(list_users)
+        return render('edit_user_confirmation.mako', request, dict(form=form, action=request.path))
+  else:
+    form = AddLdapUserForm()
+  return render('edit_user.mako', request, dict(form=form, action=request.path))
+
+class AddLdapGroupForm(forms.Form):
+  name = forms.RegexField(
+      label="Name",
+      max_length=64,
+      regex='^%s$' % (get_groupname_re_rule(),),
+      help_text="Required. 30 characters or fewer. May only contain letters, "
+                "numbers, hypens or underscores.",
+      error_messages={'invalid': "Whitespaces and ':' not allowed" })
+  dn = forms.BooleanField(label="Distinguished name",
+                          help_text="Whether or not the group should be imported by "
+                                    "distinguished name",
+                          initial=False,
+                          required=False)
+  import_members = forms.BooleanField(label='Import new members',
+                                      help_text='Import unimported or new users from the group',
+                                      initial=False,
+                                      required=False)
+
+  def clean(self):
+    cleaned_data = super(AddLdapGroupForm, self).clean()
+    name = cleaned_data.get("name")
+    dn = cleaned_data.get("dn")
+
+    if not dn:
+      if name is not None and len(name) > 30:
+        msg = 'Too long: 30 characters or fewer and not %s' % (len(name),)
+        errors = self._errors.setdefault('name', ErrorList())
+        errors.append(msg)
+        raise forms.ValidationError(msg)
+
+    return cleaned_data
+
+def add_ldap_group(request):
+  """
+  add_ldap_group(request) -> reply
+
+  Handler for importing LDAP groups into the Hue database.
+
+  If a group has been previously imported, this will sync membership within the
+  group with the LDAP server. If --import-members is specified, it will import
+  all unimported users.
+  """
+  if not request.user.is_superuser:
+    raise PopupException("You must be a superuser to add another group.")
+
+  if request.method == 'POST':
+    form = AddLdapGroupForm(request.POST)
+    if form.is_valid():
+      groupname = form.cleaned_data['name']
+      import_by_dn = form.cleaned_data['dn']
+      import_members = form.cleaned_data['import_members']
+      group = import_ldap_group(groupname, import_members, import_by_dn)
+
+      if group is None:
+        errors = form._errors.setdefault('name', ErrorList())
+        errors.append('Could not get LDAP details for group %s' % (groupname,))
+      else:
+        request.path = urlresolvers.reverse(list_groups)
+        return render('edit_group_confirmation.mako', request, dict(form=form, action=request.path))
+  else:
+    form = AddLdapGroupForm()
+  return render('edit_group.mako', request, dict(form=form, action=request.path))
+
+def sync_ldap_users_groups(request):
+  """
+  Handler for syncing the Hue database with LDAP users and groups.
+
+  This will not import any users or groups that don't already exist in Hue. All
+  user information and group memberships will be updated based on the LDAP
+  server's current state.
+  """
+  if not request.user.is_superuser:
+    raise PopupException("You must be a superuser to sync the LDAP users/groups.")
+
+  if request.method == 'POST':
+    sync_ldap_users_and_groups()
+    return render('edit_user_confirmation.mako', request, {})
+  else:
+    return render('sync_ldap_users_groups.mako', request, dict(action=request.path))
 
 def _check_remove_last_super(user_obj):
   """Raise an error if we're removing the last superuser"""
@@ -438,10 +574,10 @@ def _import_ldap_group(groupname, import_members=False, import_by_dn=False):
   return group
 
 def import_ldap_user(user, import_by_dn):
-  _import_ldap_user(user, import_by_dn)
+  return _import_ldap_user(user, import_by_dn)
 
 def import_ldap_group(group, import_members, import_by_dn):
-  _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():
   """