cobbler support for users & tags

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

 



The attached patch is the first step towards an authorization system
for cobbler.    It only adds tags for systems and user support.   The
tags do nothing yet, but will come into play with later patches.

Michael, you can apply if you want or do the sensible thing and wait
until this does something useful.    I'll try to push my branch to the
public repository later if people want to try that rather than
patches.

The authorization support I have in mind uses these generic tags to
grant users access to systems and profiles.     I think profiles will
have inheritable tags, but will not be editable by non-superuser
users, since this is probably what most people want.    Basically, if
a user has a tag that a system (or its upstream profile(s)) also has,
they have r/w access.   Otherwise, it's a deny-all policy.    Users
can be granted superuser access with the --superuser flag which is
only available on the CLI for now.

It looks like it will be really easy to support authorization in both
the webui and CLI.   The CLI support will come via sudo and its
SUDO_USER environment variable.   That way users can be given access
to run the CLI as root, but only for given systems.   It will be up to
each sysadmin out there to determine whether they want to risk giving
sudo access to cobbler as root and trust cobbler's code.

I'm definitely open to discussion about how the authorization stuff
plays out.   Right now I'm sticking to the KISS principle and trying
to keep things very flexible.

-Al
From 0beb3357bb1d30c3530ede59051712df92e78d08 Mon Sep 17 00:00:00 2001
From: Al Tobey <tobert@xxxxxxxxx>
Date: Sun, 4 Nov 2007 16:11:53 -0800
Subject: [PATCH] Add users and tags.
    * add user objects and all the bits need for them
      - they don't do anything yet
    * add tags to systems and profiles
      - also useless so far
      - will be used for user authorization
      - will be used for UI updates to split systems up logically for nicer reports

---
 cobbler.spec                      |    1 +
 cobbler/action_sync.py            |    1 +
 cobbler/api.py                    |   12 ++++
 cobbler/cobbler.py                |   71 +++++++++++++++++++++++++-
 cobbler/collection.py             |    3 +
 cobbler/collection_users.py       |   57 ++++++++++++++++++++
 cobbler/config.py                 |   21 +++++++-
 cobbler/item_profile.py           |   17 ++++++
 cobbler/item_system.py            |   16 ++++++
 cobbler/item_user.py              |  103 +++++++++++++++++++++++++++++++++++++
 cobbler/remote.py                 |   70 ++++++++++++++++++++++++-
 cobbler/webui/CobblerWeb.py       |   98 ++++++++++++++++++++++++++++++++++-
 cobbler/webui/master.py           |   38 ++++++++-----
 docs/cobbler.pod                  |   50 ++++++++++++++++--
 setup.py                          |    8 +++-
 webui_templates/master.tmpl       |    2 +
 webui_templates/profile_edit.tmpl |   15 +++++
 webui_templates/profile_list.tmpl |    3 +
 webui_templates/system_edit.tmpl  |   14 +++++
 webui_templates/system_list.tmpl  |    5 ++
 webui_templates/user_edit.tmpl    |   99 +++++++++++++++++++++++++++++++++++
 webui_templates/user_list.tmpl    |   41 +++++++++++++++
 22 files changed, 716 insertions(+), 29 deletions(-)
 create mode 100644 cobbler/collection_users.py
 create mode 100644 cobbler/item_user.py
 create mode 100644 webui_templates/user_edit.tmpl
 create mode 100644 webui_templates/user_list.tmpl

diff --git a/cobbler.spec b/cobbler.spec
index 26a5d62..4c5fa3b 100644
--- a/cobbler.spec
+++ b/cobbler.spec
@@ -178,6 +178,7 @@ test "x$RPM_BUILD_ROOT" != "x" && rm -rf $RPM_BUILD_ROOT
 
 %defattr(664,root,root)
 %config(noreplace) /var/lib/cobbler/settings
+%config(noreplace) /var/lib/cobbler/users
 %config(noreplace) /var/lib/cobbler/snippets/partition_select
 /var/lib/cobbler/elilo-3.6-ia64.efi
 /var/lib/cobbler/menu.c32
diff --git a/cobbler/action_sync.py b/cobbler/action_sync.py
index 764958c..6187205 100644
--- a/cobbler/action_sync.py
+++ b/cobbler/action_sync.py
@@ -53,6 +53,7 @@ class BootSync:
         self.systems  = config.systems()
         self.settings = config.settings()
         self.repos    = config.repos()
+        self.users    = config.users()
         self.load_snippet_cache()
 
     def run(self):
diff --git a/cobbler/api.py b/cobbler/api.py
index 2a51c1a..f36996c 100644
--- a/cobbler/api.py
+++ b/cobbler/api.py
@@ -89,6 +89,12 @@ class BootAPI:
         """
         return self._config.repos()
 
+    def users(self):
+        """
+        Return the current list of users
+        """
+        return self._config.users()
+
     def settings(self):
         """
         Return the application configuration
@@ -120,6 +126,12 @@ class BootAPI:
         """
         return self._config.new_repo(is_subobject=is_subobject)
 
+    def new_user(self,is_subobject=False):
+        """
+        Create a blank, unconfigured user, unattached to a collection
+        """
+        return self._config.new_user()
+
     def auto_add_repos(self):
         """
         Import any repos this server knows about and mirror them.
diff --git a/cobbler/cobbler.py b/cobbler/cobbler.py
index 78351f6..6bb6013 100755
--- a/cobbler/cobbler.py
+++ b/cobbler/cobbler.py
@@ -83,6 +83,16 @@ class BootCLI:
             'report'   :  self.repo_report,
             'sync'     :  self.reposync
         }
+        self.commands['user'] = {
+            'add'      :  self.user_add,
+            'edit'     :  self.user_edit,
+            'rename'   :  self.user_rename,
+            'copy'     :  self.user_copy,
+            'delete'   :  self.user_remove,
+            'remove'   :  self.user_remove,
+            'list'     :  self.user_list,
+            'report'   :  self.user_report,
+        }
         self.commands['toplevel'] = {
             '-v'           : self.version,
             '--version'    : self.version,
@@ -98,6 +108,7 @@ class BootCLI:
             'system'       : self.system,
             'repos'        : self.repo,
             'repo'         : self.repo,
+            'user'         : self.user,
             'sync'         : self.sync,
             'reposync'     : self.reposync,
             'import'       : self.import_tree,
@@ -150,6 +161,12 @@ class BootCLI:
         else:
            return self.__print_sorted(self.api.repos())
 
+    def user_report(self,args):
+        if len(args) > 0:
+           return self.__list_names2(self.api.users(), args)
+        else:
+           return self.__print_sorted(self.api.users())
+
     def report(self,args):
 
         args.append("") # filler
@@ -167,6 +184,9 @@ class BootCLI:
             if a == '--systems' or len(args) == 1:
                 self.system_report([])
                 match = True
+            if a == '--users' or len(args) == 1:
+                self.user_report([])
+                match = True
             if not match and a is not None and a != "":
                 raise CX(_("cobbler does not understand '%(command)s'") % { "command" : a })
             match = False
@@ -227,6 +247,12 @@ class BootCLI:
            return self.__list_names2(self.api.repos(),args)
         else:
            return self.__list_names(self.api.repos())
+ 
+    def user_list(self, args):
+        if len(args) > 0:
+           return self.__list_names2(self.api.users(),args)
+        else:
+           return self.__list_names(self.api.users())
 
     ###############################################################
     # UTILITY FUNCTIONS
@@ -322,6 +348,9 @@ class BootCLI:
     
     def repo_remove(self,args):
         return self.__generic_remove(args,"repo","name",self.api.repos)
+
+    def user_remove(self,args):
+        return self.__generic_remove(args,"user","name",self.api.users)
  
     ####################################################################
     # COPY FUNCTIONS
@@ -342,6 +371,10 @@ class BootCLI:
         exc = _("repository does not exist")
         self.__generic_copy(args,self.api.repos,self.__repo_control,exc)
 
+    def user_copy(self,args):
+        exc = _("user does not exist")
+        self.__generic_copy(args,self.api.users,self.__user_control,exc)
+
     #####################################################################
     # RENAME FUNCTIONS
 
@@ -361,6 +394,10 @@ class BootCLI:
         exc = _("repository does not exist")
         self.__generic_rename(args,self.api.repos,self.__repo_control,exc)
 
+    def user_rename(self,args):
+        exc = _("user does not exist")
+        self.__generic_rename(args,self.api.users,self.__user_control,exc)
+
     #####################################################################
     # EDIT FUNCTIONS
 
@@ -375,11 +412,15 @@ class BootCLI:
     def system_edit(self,args):
         exc = _("system does not exist")
         self.__generic_edit(args,self.api.systems,self.__system_control,exc)
-    
+
     def repo_edit(self,args):
         exc = _("repository does not exist")
         self.__generic_edit(args,self.api.repos,self.__repo_control,exc)
-   
+
+    def user_edit(self,args):
+        exc = _("user does not exist")
+        self.__generic_edit(args,self.api.users,self.__user_control,exc)
+
     #####################################################################
     # ADD FUNCTIONS
 
@@ -421,6 +462,8 @@ class BootCLI:
         does_inherit = self.__prescan_for_inheritance_args(args)
         self.__generic_add(args,self.api.new_repo,self.__repo_control,does_inherit)
 
+    def user_add(self,args):
+        self.__generic_add(args,self.api.new_user,self.__user_control,None)
 
     ###############################################################
     # CONTROL IMPLEMENTATIONS
@@ -439,6 +482,7 @@ class BootCLI:
             '--kick-start'      :  lambda(a) : profile.set_kickstart(a),
             '--answers'         :  lambda(a) : profile.set_kickstart(a),
             '--kopts'           :  lambda(a) : profile.set_kernel_options(a),
+            '--tags'            :  lambda(a) : profile.set_tags(a),
             '--virt-file-size'  :  lambda(a) : profile.set_virt_file_size(a),
             '--virt-ram'        :  lambda(a) : profile.set_virt_ram(a),
             '--virt-bridge'     :  lambda(a) : profile.set_virt_bridge(a),
@@ -578,6 +622,7 @@ class BootCLI:
            '--dhcp-tag7'       : lambda(a) : sys.set_dhcp_tag(a,"intf7"),
            '--kickstart'       : lambda(a) : sys.set_kickstart(a),
            '--netboot-enabled' : lambda(a) : sys.set_netboot_enabled(a),
+           '--tags'            : lambda(a) : sys.set_tags(a),
            '--virt-path'       : lambda(a) : sys.set_virt_path(a),
            '--virt-type'       : lambda(a) : sys.set_virt_type(a),
            '--server-override' : lambda(a) : sys.set_server(a)
@@ -587,6 +632,22 @@ class BootCLI:
             self.api.systems().add(sys, with_copy=True)
         return self.apply_args(args,commands,on_ok)
 
+    def __user_control(self,args,user,newname=None):
+        """
+        Create/edit a user:  'cobbler user add --name='joe' --tags="foo,bar" --superuser=[y/n] ...
+        """
+        commands = {
+           '--name'             :  lambda(a): user.set_name(a),
+           '--newname'          :  lambda(a): True,
+           '--tags'             :  lambda(a): user.set_tags(a),
+           '--superuser'        :  lambda(a): user.set_superuser(a)
+        }
+        def on_ok():
+            if newname is not None:
+                user.set_name(newname)
+            self.api.users().add(user, with_copy=True)
+        return self.apply_args(args,commands,on_ok)
+
     ###################################################################################
     # PARSING FUNCTIONS
 
@@ -763,6 +824,12 @@ class BootCLI:
         """
         return self.relay_args(args, self.commands['repo'])
 
+    def user(self,args):
+        """
+        Handles any of the 'cobbler user' subcommands
+        """
+        return self.relay_args(args, self.commands['user'])
+
 ####################################################
 
 def main():
diff --git a/cobbler/collection.py b/cobbler/collection.py
index 8e6be39..014517c 100644
--- a/cobbler/collection.py
+++ b/cobbler/collection.py
@@ -24,6 +24,7 @@ import item_system
 import item_profile
 import item_distro
 import item_repo
+import item_user
 
 from rhpl.translate import _, N_, textdomain, utf8
 
@@ -142,6 +143,8 @@ class Collection(serializable.Serializable):
                 lite_sync.add_single_distro(ref.name)
             elif isinstance(ref, item_repo.Repo):
                 pass
+            elif isinstance(ref, item_user.User):
+                pass
             else:
                 print _("Internal error. Object type not recognized: %s") % type(ref)
         
diff --git a/cobbler/collection_users.py b/cobbler/collection_users.py
new file mode 100644
index 0000000..cf710b1
--- /dev/null
+++ b/cobbler/collection_users.py
@@ -0,0 +1,57 @@
+"""
+Cobbler user collection.
+
+Copyright 2006, Red Hat, Inc
+Michael DeHaan <mdehaan@xxxxxxxxxx>
+Albert Tobey <tobert@xxxxxxxxx>
+
+This software may be freely redistributed under the terms of the GNU
+general public license.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+"""
+
+import item_user as user
+import utils
+import collection
+from cexceptions import *
+from rhpl.translate import _, N_, textdomain, utf8
+
+TESTMODE = False
+
+#--------------------------------------------
+
+class Users(collection.Collection):
+
+    def collection_type(self):
+        return "user"
+
+    def factory_produce(self,config,seed_data):
+        """
+        Return a system forged from seed_data
+        """
+        return user.User(config).from_datastruct(seed_data)
+
+    def remove(self,name,with_delete=True):
+        """
+        Remove element named 'name' from the collection
+        """
+
+        name = name.lower()
+        obj = self.find(name=name)
+
+        if obj is not None:
+            if with_delete:
+                self._run_triggers(obj, "/var/lib/cobbler/triggers/delete/user/pre/*")
+
+            del self.listing[name]
+            self.config.serialize_delete(self, obj)
+
+            if with_delete:
+                self._run_triggers(obj, "/var/lib/cobbler/triggers/delete/user/post/*")
+
+            return True
+        raise CX(_("cannot delete an object that does not exist"))
+
diff --git a/cobbler/config.py b/cobbler/config.py
index 258b241..ddf897c 100644
--- a/cobbler/config.py
+++ b/cobbler/config.py
@@ -19,11 +19,13 @@ import item_distro as distro
 import item_profile as profile
 import item_system as system
 import item_repo as repo
+import item_user as user
 
 import collection_distros as distros
 import collection_profiles as profiles
 import collection_systems as systems
 import collection_repos as repos
+import collection_users as users
 import modules.serializer_yaml as serializer_yaml
 
 import settings
@@ -58,19 +60,22 @@ class Config:
        self._repos        = repos.Repos(weakref.proxy(self))
        self._profiles     = profiles.Profiles(weakref.proxy(self))
        self._systems      = systems.Systems(weakref.proxy(self))
+       self._users        = users.Users(weakref.proxy(self))
        self._settings     = settings.Settings() # not a true collection
        self._serialize_graph_classes = [
           self._distros,
           self._repos,
           self._profiles,
-          self._systems
+          self._systems,
+          self._users
        ]
        self._graph_classes = [
           self._settings,
           self._distros,
           self._repos,
           self._profiles,
-          self._systems
+          self._systems,
+          self._users
        ]
        
    def __cmp(self,a,b):
@@ -106,6 +111,12 @@ class Config:
        """
        return self._repos
 
+   def users(self):
+       """
+       Return the definitive copy of the Users collection
+       """
+       return self._users
+
    def new_distro(self,is_subobject=False):
        """
        Create a new distro object with a backreference to this object
@@ -130,6 +141,12 @@ class Config:
        """
        return repo.Repo(weakref.proxy(self),is_subobject=is_subobject)
 
+   def new_user(self,is_subobject=False):
+       """
+       Create a new user.
+       """
+       return user.User(weakref.proxy(self),is_subobject=False)
+
    def clear(self):
        """
        Forget about all loaded configuration data
diff --git a/cobbler/item_profile.py b/cobbler/item_profile.py
index ddb36ba..b494e95 100644
--- a/cobbler/item_profile.py
+++ b/cobbler/item_profile.py
@@ -43,6 +43,7 @@ class Profile(item.Item):
         self.virt_ram        = (512,                               '<<inherit>>')[is_subobject]
         self.repos           = ([],                                '<<inherit>>')[is_subobject]
         self.depth           = 1
+        self.tags            = ([],                                '<<inherit>>')[is_subobject]
         self.virt_type       = (self.settings.default_virt_type,   '<<inherit>>')[is_subobject]
         self.virt_path       = ("",                                '<<inherit>>')[is_subobject]
         self.virt_bridge     = (self.settings.default_virt_bridge, '<<inherit>>')[is_subobject]
@@ -65,6 +66,7 @@ class Profile(item.Item):
         self.depth           = self.load_item(seed_data,'depth', 1)     
         self.dhcp_tag        = self.load_item(seed_data,'dhcp_tag', 'default')
         self.server          = self.load_item(seed_data,'server', '<<inherit>>')
+        self.tags            = self.load_item(seed_data,'tags', [])
 
         # backwards compatibility
         if type(self.repos) != list:
@@ -183,6 +185,18 @@ class Profile(item.Item):
             return True
         raise CX(_("kickstart not found"))
 
+    def set_tags(self,tags):
+        """
+        A list of tags for the profile.  Completely arbitrary and user-defined.
+        """
+        if hasattr(tags, 'split'):
+            tags = [ tag.strip() for tag in tags.split(',') ]
+        elif not hasattr(tags, '__iter__'):
+            raise CX(_("Invalid type for tags.   Must be a string (comma-delimited) or list."))
+
+        self.tags = tags
+        return True
+
     def set_virt_cpus(self,num):
         """
         For Virt only.  Set the number of virtual CPUs to give to the
@@ -337,6 +351,7 @@ class Profile(item.Item):
             'repos'            : self.repos,
             'parent'           : self.parent,
             'depth'            : self.depth,
+            'tags'             : self.tags,
             'virt_type'        : self.virt_type,
             'virt_path'        : self.virt_path,
             'dhcp_tag'         : self.dhcp_tag,
@@ -355,6 +370,7 @@ class Profile(item.Item):
         buf = buf + _("kickstart       : %s\n") % self.kickstart
         buf = buf + _("kernel options  : %s\n") % self.kernel_options
         buf = buf + _("ks metadata     : %s\n") % self.ks_meta
+        buf = buf + _("tags            : %s\n") % self.tags
         buf = buf + _("virt file size  : %s\n") % self.virt_file_size
         buf = buf + _("virt ram        : %s\n") % self.virt_ram
         buf = buf + _("virt type       : %s\n") % self.virt_type
@@ -377,6 +393,7 @@ class Profile(item.Item):
             'virt-file-size'  :  self.set_virt_file_size,
             'virt-ram'        :  self.set_virt_ram,
             'ksmeta'          :  self.set_ksmeta,
+            'tags'            :  self.set_tags,
             'repos'           :  self.set_repos,
             'virt-path'       :  self.set_virt_path,
             'virt-type'       :  self.set_virt_type,
diff --git a/cobbler/item_system.py b/cobbler/item_system.py
index 9fa4504..739bfa1 100644
--- a/cobbler/item_system.py
+++ b/cobbler/item_system.py
@@ -37,6 +37,7 @@ class System(item.Item):
         self.netboot_enabled = (1,        '<<inherit>>')[is_subobject] 
         self.depth           = 2
         self.kickstart       = "<<inherit>>"   # use value in profile
+        self.tags            = "<<inherit>>"   # ""
         self.virt_path       = "<<inherit>>"   # ""
         self.virt_type       = "<<inherit>>"   # "" 
         self.server          = "<<inherit>>"   # "" (or settings)
@@ -85,6 +86,7 @@ class System(item.Item):
         self.ks_meta         = self.load_item(seed_data, 'ks_meta', {})
         self.depth           = self.load_item(seed_data, 'depth', 2)        
         self.kickstart       = self.load_item(seed_data, 'kickstart', '<<inherit>>')
+        self.tags            = self.load_item(seed_data, 'tags', '<<inherit>>') 
         self.virt_path       = self.load_item(seed_data, 'virt_path', '<<inherit>>') 
         self.virt_type       = self.load_item(seed_data, 'virt_type', '<<inherit>>')
         self.netboot_enabled = self.load_item(seed_data, 'netboot_enabled', 1)
@@ -261,6 +263,18 @@ class System(item.Item):
             return True
         raise CX(_("invalid profile name"))
 
+    def set_tags(self,tags):
+        """
+        A list of tags for the server.   Completely arbitrary and user-defined.
+        """
+        if hasattr(tags, 'split'):
+            tags = [ tag.strip() for tag in tags.split(',') ]
+        elif not hasattr(tags, '__iter__'):
+            raise CX(_("Invalid type for tags.   Must be a string (comma-delimited) or list."))
+
+        self.tags = tags
+        return True
+
     def set_virt_path(self,path):
         """
         Virtual storage location suggestion, can be overriden by koan.
@@ -340,6 +354,7 @@ class System(item.Item):
            'parent'          : self.parent,
            'depth'           : self.depth,
            'kickstart'       : self.kickstart,
+           'tags'            : self.tags,
            'virt_type'       : self.virt_type,
            'virt_path'       : self.virt_path,
            'interfaces'      : self.interfaces,
@@ -398,6 +413,7 @@ class System(item.Item):
            'hostname'         : self.set_hostname,
            'kickstart'        : self.set_kickstart,
            'netboot-enabled'  : self.set_netboot_enabled,
+           'tags'             : self.set_tags,
            'virt-path'        : self.set_virt_path,
            'virt-type'        : self.set_virt_type,
            'modify-interface' : self.modify_interface,
diff --git a/cobbler/item_user.py b/cobbler/item_user.py
new file mode 100644
index 0000000..ecf436d
--- /dev/null
+++ b/cobbler/item_user.py
@@ -0,0 +1,103 @@
+"""
+A Cobbler user.   Just for the webui at the moment, but support for environment
+varibles other than REMOTE_USER (e.g. SUDO_USER) may be added in the future.
+
+Copyright 2006, Red Hat, Inc
+Michael DeHaan <mdehaan@xxxxxxxxxx>
+Albert Tobey <tobert@xxxxxxxxx>
+
+This software may be freely redistributed under the terms of the GNU
+general public license.
+
+You should have received a copy of the GNU General Public License
+along with this program; if not, write to the Free Software
+Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
+"""
+
+import utils
+import item
+from cexceptions import *
+from rhpl.translate import _, N_, textdomain, utf8
+
+class User(item.Item):
+
+    TYPE_NAME = _("user")
+    COLLECTION_TYPE = "user"
+
+    def make_clone(self):
+        ds = self.to_datastruct()
+        cloned = User(self.config)
+        cloned.from_datastruct(ds)
+        return cloned
+
+    def clear(self,is_subobject=False):
+        self.name      = None
+        self.tags      = []
+        self.superuser = 0
+
+    def from_datastruct(self,seed_data):
+        self.name      = self.load_item(seed_data, 'name')
+        self.tags      = self.load_item(seed_data, 'tags')
+        self.superuser = self.load_item(seed_data, 'superuser')
+        return self
+
+    def set_name(self,name):
+        """
+        The user name.  Think system user name.
+        """
+        self.name = name
+        return True
+
+    def set_tags(self,tags):
+        """
+        A list of tags for the user.
+        """
+        if hasattr(tags, 'split'):
+            tags = [ tag.strip() for tag in tags.split(',') ]
+        elif not hasattr(tags, '__iter__'):
+            raise CX(_("Invalid type for tags.   Must be a string (comma-delimited) or list."))
+
+        self.tags = tags
+        return True
+
+    def set_superuser(self,superuser):
+        """
+	    Specify whether or not a user has full access or not.
+	    """
+        if type(superuser) == bool:
+            self.superuser = superuser
+            return True
+        if not str(superuser).lower() in ["yes","y","yup","yeah","1","true"]:  
+            self.superuser = False
+        else:
+            self.superuser = True
+        return True
+
+    def is_valid(self):
+        """
+	    A user must have a name to be valid.
+	    """
+        if self.name is None:
+            return False
+        return True
+
+    def to_datastruct(self):
+        return {
+           'name'             : self.name,
+           'tags'             : self.tags,
+           'superuser'        : self.superuser
+        }
+
+    def printable(self):
+        buf =       _("user             : %s\n") % self.name
+        buf = buf + _("tags             : %s\n") % ', '.join(self.tags)
+        buf = buf + _("superuser flag   : %s\n") % self.superuser
+        return buf
+
+    def remote_methods(self):
+        # set_superuser not allowed remotely, only on command line
+        return {
+            'name'             :  self.set_name,
+            'tags'             :  self.set_tags
+        }
+
diff --git a/cobbler/remote.py b/cobbler/remote.py
index 7b92a17..cc449ca 100644
--- a/cobbler/remote.py
+++ b/cobbler/remote.py
@@ -32,6 +32,7 @@ import item_distro
 import item_profile
 import item_system
 import item_repo
+import item_user
 
 config_parser = ConfigParser.ConfigParser()
 auth_conf = open("/etc/cobbler/auth.conf")
@@ -182,6 +183,12 @@ class CobblerXMLRPCInterface:
         """
         return self.__get_all("repo",page,results_per_page)
 
+    def get_users(self,page=None,results_per_page=None,token=None):
+        """
+        Returns all cobbler users as an array of hashes.
+        """
+        return self.__get_all("user",page,results_per_page)
+
     def __get_specific(self,collection_fn,name,flatten=False):
         """
         Internal function to return a hash representation of a given object if it exists,
@@ -220,6 +227,12 @@ class CobblerXMLRPCInterface:
         """
         return self.__get_specific(self.api.repos,name,flatten=flatten)
 
+    def get_user(self,name,flatten=False,token=None):
+        """
+        Returns the user named "name" as a hash.
+        """
+        return self.__get_specific(self.api.users,name,flatten=flatten)
+
     def get_distro_as_rendered(self,name,token=None):
         """
         Return the distribution as passed through cobbler's
@@ -545,7 +558,15 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface):
         """
         self.__validate_token(token)
         return self.__store_object(item_repo.Repo(self.api._config))
-       
+ 
+    def new_user(self,token):
+        """
+        Creates a new empty user object.  See the documentation 
+        for new_distro as it works exactly the same.
+        """
+        self.__validate_token(token)
+        return self.__store_object(item_user.User(self.api._config))
+
     def get_distro_handle(self,name,token):
         """
         Given the name of an distro (or other search parameters), return an
@@ -582,7 +603,7 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface):
     def get_repo_handle(self,name,token):
         """
         Given the name of an repo (or other search parameters), return an
-        object id that can be passed in to modify_repo() or save_pro()
+        object id that can be passed in to modify_repo() or save_repo()
         commands.  Raises an exception if no object can be matched.
         """
         self.__validate_token(token)
@@ -590,6 +611,17 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface):
         found = self.api.repos().find(name)
         return self.__store_object(found)   
 
+    def get_user_handle(self,name,token):
+        """
+        Given the name of an user (or other search parameters), return an
+        object id that can be passed in to modify_user() or save_user()
+        commands.  Raises an exception if no object can be matched.
+        """
+        self.__validate_token(token)
+        self._refresh()
+        found = self.api.users().find(name)
+        return self.__store_object(found)   
+
     def save_distro(self,object_id,token):
         """
         Saves a newly created or modified distro object to disk.
@@ -622,6 +654,14 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface):
         obj = self.__get_object(object_id)
         return self.api.repos().add(obj,with_copy=True)
 
+    def save_user(self,object_id,token=None):
+        """
+        Saves a newly created or modified user object to disk.
+        """
+        self.__validate_token(token)
+        obj = self.__get_object(object_id)
+        return self.api.users().add(obj,with_copy=True)
+
     def __call_method(self, obj, attribute, arg):
         """
         Internal function used by the modify routines.
@@ -667,6 +707,15 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface):
         obj = self.__get_object(object_id)
         return self.__call_method(obj, attribute, arg)
 
+    def modify_user(self,object_id,attribute,arg,token):
+        """
+        Allows modification of certain attributes on newly created or
+        existing user object handle.
+        """
+        self.__validate_token(token)
+        obj = self.__get_object(object_id)
+        return self.__call_method(obj, attribute, arg)
+
     def distro_remove(self,name,token):
         """
         Deletes a distro from a collection.  Note that this just requires the name
@@ -703,6 +752,15 @@ class CobblerReadWriteXMLRPCInterface(CobblerXMLRPCInterface):
         rc = self.api._config.repos().remove(name)
         return rc
 
+    def user_remove(self,name,token):
+        """
+        Deletes a user from a collection.  Note that this just requires the name
+        of the user, not a handle.
+        """
+        self.__validate_token(token)
+        rc = self.api._config.users().remove(name)
+        return rc
+
     def sync(self,token): 
         """
         Applies changes in Cobbler to the filesystem.
@@ -839,6 +897,13 @@ if __name__ == "__main__":
     remote.modify_repo(repo_id, 'mirror', 'rsync://mirror.example.org/foo', token)
     remote.save_repo(repo_id, token)
 
+    # now add a user
+    user_id = remote.new_user(token)
+    remote.modify_user(user_id, 'name',   'testuser', token)
+    remote.modify_user(user_id, 'tags', 'foo, bar , baz', token)
+    remote.modify_user(user_id, 'tags', 'barf, foo, bar', token)
+    remote.save_user(user_id, token)
+
     # now add a profile
     profile_id = remote.new_profile(token)
     remote.modify_profile(profile_id, 'name',      'example-profile', token)
@@ -857,6 +922,7 @@ if __name__ == "__main__":
     print remote.get_profiles()
     print remote.get_systems()
     print remote.get_repos()
+    print remote.get_users()
 
     print remote.get_system("AA:BB:AA:BB:AA:BB",True) # flattened
 
diff --git a/cobbler/webui/CobblerWeb.py b/cobbler/webui/CobblerWeb.py
index 169cd99..b3ad9a9 100644
--- a/cobbler/webui/CobblerWeb.py
+++ b/cobbler/webui/CobblerWeb.py
@@ -378,7 +378,7 @@ class CobblerWeb(object):
     def system_save(self,name=None,oldname=None,editmode="edit",profile=None,
                     new_or_edit=None,  
                     kopts=None, ksmeta=None, server_override=None, netboot='n', 
-                    delete1=None, delete2=None, **args):
+                    tags=None, delete1=None, delete2=None, **args):
 
         if not self.__xmlrpc_setup():
             return self.xmlrpc_auth_failure()
@@ -437,6 +437,8 @@ class CobblerWeb(object):
                self.remote.modify_system(system, 'netboot-enabled', netboot, self.token)
             if server_override:
                self.remote.modify_system(system, 'server', server_override, self.token)
+            if tags:
+               self.remote.modify_system(system, 'tags', tags, self.token)
 
             for x in range(0,7):
                 interface = "intf%s" % x
@@ -543,7 +545,7 @@ class CobblerWeb(object):
         } )
 
     def profile_save(self,new_or_edit=None,editmode='edit',name=None,oldname=None,
-                     distro=None,kickstart=None,kopts=None,
+                     distro=None,kickstart=None,kopts=None,tags=None,
                      ksmeta=None,virtfilesize=None,virtram=None,virttype=None,
                      virtpath=None,repos=None,dhcptag=None,delete1=None,delete2=None,
                      parent=None,virtcpus=None,virtbridge=None,subprofile=None,server_override=None,**args):
@@ -596,6 +598,8 @@ class CobblerWeb(object):
                 self.remote.modify_profile(profile, 'kopts', kopts, self.token)
             if ksmeta:
                 self.remote.modify_profile(profile, 'ksmeta', ksmeta, self.token)
+            if tags:
+                self.remote.modify_profile(profile, 'tags', tags, self.token)
             if virtfilesize:
                 self.remote.modify_profile(profile, 'virt-file-size', virtfilesize, self.token)
             if virtram:
@@ -734,6 +738,92 @@ class CobblerWeb(object):
         return self.repo_list()
 
     # ------------------------------------------------------------------------ #
+    # Users
+    # ------------------------------------------------------------------------ #
+
+    def user_list(self,page=None,limit=None):
+        if not self.__xmlrpc_setup():
+            return self.xmlrpc_auth_failure()
+
+        (page, results_per_page, pages) = self.__compute_pagination(page,limit,"user")
+        users = self.remote.get_users(page,results_per_page)
+
+        if len(users) > 0:
+            return self.__render( 'user_list.tmpl', {
+                'users'            : users,
+                'pages'            : pages,
+                'page'             : page,
+                'results_per_page' : results_per_page
+            })
+        else:
+            return self.__render('empty.tmpl', {})
+
+    def user_edit(self, name=None):
+        if not self.__xmlrpc_setup():
+            return self.xmlrpc_auth_failure()
+
+        input_user = None
+        if name is not None:
+            input_user = self.remote.get_user(name, True)
+            input_user['superuser'] = self.__truth(input_user['superuser'])
+
+        return self.__render( 'user_edit.tmpl', {
+            'user': input_user,
+        } )
+
+    def user_save(self,name=None,oldname=None,new_or_edit=None,editmode="edit",
+                  superuser=None,tags=None,delete1=None,delete2=None,**args):
+        if not self.__xmlrpc_setup():
+            return self.xmlrpc_auth_failure()
+
+        if superuser is not None:
+            return self.error_page("Nice try, chimp.  Move along ...")
+
+        # pre-command parameter checking
+        if name is None and editmode=='edit' and oldname is not None:
+            name = oldname
+        if name is None:
+            return self.error_page("name is required")
+        if (editmode == 'rename' or editmode == 'copy') and name == oldname:
+            return self.error_page("The name has not been changed.")
+
+        # handle deletes as a special case
+        if new_or_edit == 'edit' and delete1 and delete2:
+            try:
+                self.remote.user_remove(name,self.token)
+            except Exception, e:
+                return self.error_page("could not delete %s, %s" % (name,str(e)))
+            return self.user_list()
+
+        # grab a reference to the object
+        if new_or_edit == "edit" and editmode == "edit":
+            try:
+                user = self.remote.get_user_handle( name, self.token)
+            except:
+                return self.error_page("Failed to lookup user: %s" % name)
+        else:
+            user = self.remote.new_user(self.token)
+
+        try:
+            self.remote.modify_user(user, 'name', name, self.token)
+            if tags:
+               self.remote.modify_system(user, 'tags', tags, self.token)
+
+            self.remote.save_user(user, self.token)
+
+        except Exception, e:
+            log_exc()
+            return self.error_page("Error while saving user: %s" % str(e))
+
+        if editmode == "rename" and name != oldname:
+            try:
+                self.remote.user_remove(oldname, self.token)
+            except Exception, e:
+                return self.error_page("Rename unsuccessful. Object %s was copied instead, and the old copy (%s) still remains. Reason: %s" % (name, oldname, str(e)))
+
+        return self.user_list()
+
+    # ------------------------------------------------------------------------ #
     # Kickstart files
     # ------------------------------------------------------------------------ #
 
@@ -831,6 +921,10 @@ class CobblerWeb(object):
     repo_list.exposed = True
     repo_save.exposed = True
 
+    user_edit.exposed = True
+    user_list.exposed = True
+    user_save.exposed = True
+
     settings_view.exposed = True
     ksfile_edit.exposed = True
     ksfile_save.exposed = True
diff --git a/cobbler/webui/master.py b/cobbler/webui/master.py
index 9dd79d5..6d880ba 100644
--- a/cobbler/webui/master.py
+++ b/cobbler/webui/master.py
@@ -31,12 +31,12 @@ VFFSL=valueFromFrameOrSearchList
 VFSL=valueFromSearchList
 VFN=valueForName
 currentTime=time.time
-__CHEETAH_version__ = '2.0rc8'
-__CHEETAH_versionTuple__ = (2, 0, 0, 'candidate', 8)
-__CHEETAH_genTime__ = 1193780329.9635999
-__CHEETAH_genTimestamp__ = 'Tue Oct 30 17:38:49 2007'
+__CHEETAH_version__ = '2.0'
+__CHEETAH_versionTuple__ = (2, 0, 0, 'final', 0)
+__CHEETAH_genTime__ = 1194218458.5938289
+__CHEETAH_genTimestamp__ = 'Sun Nov  4 15:20:58 2007'
 __CHEETAH_src__ = 'webui_templates/master.tmpl'
-__CHEETAH_srcLastModified__ = 'Mon Oct 29 16:39:12 2007'
+__CHEETAH_srcLastModified__ = 'Sun Nov  4 15:20:50 2007'
 __CHEETAH_docstring__ = 'Autogenerated by CHEETAH: The Python-Powered Template Engine'
 
 if __CHEETAH_versionTuple__ < RequiredCheetahVersionTuple:
@@ -69,7 +69,7 @@ class master(Template):
 
 
 
-        ## CHEETAH: generated from #block body at line 53, col 1.
+        ## CHEETAH: generated from #block body at line 55, col 1.
         trans = KWS.get("trans")
         if (not trans and not self._CHEETAH__isBuffering and not callable(self.transaction)):
             trans = self.transaction # is None unless self.awake() was called
@@ -175,32 +175,40 @@ class master(Template):
         _v = VFFSL(SL,"base_url",True) # '$base_url' on line 39, col 22
         if _v is not None: write(_filter(_v, rawExpr='$base_url')) # from line 39, col 22.
         write('''/repo_list" class="menu">Repos</a></li>
+        <li><a href="''')
+        _v = VFFSL(SL,"base_url",True) # '$base_url' on line 40, col 22
+        if _v is not None: write(_filter(_v, rawExpr='$base_url')) # from line 40, col 22.
+        write('''/user_list" class="menu">Users</a></li>
         <li><hr/></li>
         <li>ADD</li>
         <li><a href="''')
-        _v = VFFSL(SL,"base_url",True) # '$base_url' on line 42, col 22
-        if _v is not None: write(_filter(_v, rawExpr='$base_url')) # from line 42, col 22.
-        write('''/distro_edit" class="menu">Distro</a></li>
-        <li><a href="''')
         _v = VFFSL(SL,"base_url",True) # '$base_url' on line 43, col 22
         if _v is not None: write(_filter(_v, rawExpr='$base_url')) # from line 43, col 22.
-        write('''/profile_edit" class="menu">Profile</a></li>
+        write('''/distro_edit" class="menu">Distro</a></li>
         <li><a href="''')
         _v = VFFSL(SL,"base_url",True) # '$base_url' on line 44, col 22
         if _v is not None: write(_filter(_v, rawExpr='$base_url')) # from line 44, col 22.
-        write('''/subprofile_edit" class="menu">Subprofile</a></li>
+        write('''/profile_edit" class="menu">Profile</a></li>
         <li><a href="''')
         _v = VFFSL(SL,"base_url",True) # '$base_url' on line 45, col 22
         if _v is not None: write(_filter(_v, rawExpr='$base_url')) # from line 45, col 22.
-        write('''/system_edit" class="menu">System</a></li>
+        write('''/subprofile_edit" class="menu">Subprofile</a></li>
         <li><a href="''')
         _v = VFFSL(SL,"base_url",True) # '$base_url' on line 46, col 22
         if _v is not None: write(_filter(_v, rawExpr='$base_url')) # from line 46, col 22.
+        write('''/system_edit" class="menu">System</a></li>
+        <li><a href="''')
+        _v = VFFSL(SL,"base_url",True) # '$base_url' on line 47, col 22
+        if _v is not None: write(_filter(_v, rawExpr='$base_url')) # from line 47, col 22.
         write('''/repo_edit" class="menu">Repo</a></li>
+        <li><a href="''')
+        _v = VFFSL(SL,"base_url",True) # '$base_url' on line 48, col 22
+        if _v is not None: write(_filter(_v, rawExpr='$base_url')) # from line 48, col 22.
+        write('''/user_edit" class="menu">User</a></li>
         <li><hr/><br/></li>
         <li><a class="button sync" href="''')
-        _v = VFFSL(SL,"base_url",True) # '$base_url' on line 48, col 42
-        if _v is not None: write(_filter(_v, rawExpr='$base_url')) # from line 48, col 42.
+        _v = VFFSL(SL,"base_url",True) # '$base_url' on line 50, col 42
+        if _v is not None: write(_filter(_v, rawExpr='$base_url')) # from line 50, col 42.
         write('''/sync">Sync</a></li>
     </ul>
 </div>
diff --git a/docs/cobbler.pod b/docs/cobbler.pod
index 5d53811..84ddd44 100644
--- a/docs/cobbler.pod
+++ b/docs/cobbler.pod
@@ -108,7 +108,7 @@ The file used for the answer file, regardless of the breed setting, is the value
 
 A profile associates a distribution to additional specialized options, such as a kickstart automation file.  Profiles are the core unit of provisioning and at least one profile must exist for every distribution to be provisioned.  A profile might represent, for instance, a web server or desktop configuration.  In this way, profiles define a role to be performed.  
 
-B<cobbler profile add --name=string --distro=string [--kickstart=path] [--kopts=string] [--ksmeta=string] [--virt-file-size=gigabytes] [--virt-ram=megabytes] [--virt-type=string] [--virt-cpus=integer] [--virt-path=string] [--virt-bridge=string] [--server-override]>
+B<cobbler profile add --name=string --distro=string [--kickstart=path] [--kopts=string] [--ksmeta=string] [--virt-file-size=gigabytes] [--virt-ram=megabytes] [--virt-type=string] [--virt-cpus=integer] [--virt-path=string] [--virt-bridge=string] [--server-override] [--tags=string]>
 
 Arguments are as listed for distributions, save for the removal of "arch" and "breed", and with the additions listed below:
 
@@ -130,6 +130,9 @@ If this parameter is not provided, the kickstart file will default to /etc/cobbl
 
 When using kickstart files, they can be placed anywhere on the filesystem, but the recommended path is /var/lib/cobbler/kickstarts.
 
+=item tags
+
+A list of basically arbitrary tags for the system.   These are used for authorization in the webui and command line when run under sudo.  See USER ACCESS CONTROL / AUTHORIZATION.
 
 =item virt-file-size
 
@@ -176,19 +179,19 @@ Example:  If profile A has --kopts="x=7 y=2", B inherits from A, and B has --kop
 Example:  If profile B has --virt-ram=256 and A has --virt-ram of 512, profile B will use the value 256.  
 Example:  If profile A has a --virt-file-size of 5 and B does not specify a size, B will use the value from A.
 
-=back
-
 =item server-override
 
 This parameter should be useful only in select circumstances.  If machines are on a subnet that cannot access the cobbler server using the name/IP as configured in the cobbler settings file, use this parameter to override that server name.   See also --dhcp-tag for configuring the next server and DHCP informmation of the system if you are also using Cobbler to help manage your DHCP configuration.
 
+=back
+
 =head2 ADDING A SYSTEM
 
 System records map a piece of hardware (or a virtual machine) with the cobbler profile to be assigned to run on it.  This may be thought of as chosing a role for a specific system.
 
 Note that if provisioning via koan and PXE menus alone, it is not required to create system records, though they are useful when system specific customizations are required.   One such customization would be defining the MAC address.  If there is a specific role inteded for a given machine, system records should be created for it.
 
-B<cobbler system add --name=string --profile=string [--mac=macaddress] [--ip=ipaddress] [--hostname=hostname] [--kopts=string] [--ksmeta=string] [--kickstart=path] [--netboot-enabled=Y/N] [--server-override=string]>
+B<cobbler system add --name=string --profile=string [--mac=macaddress] [--ip=ipaddress] [--hostname=hostname] [--kopts=string] [--ksmeta=string] [--kickstart=path] [--netboot-enabled=Y/N] [--server-override=string] [--tags=taglist]>
 
 Adds a cobbler System to the configuration.  Arguments are specified as per "profile add" with
 the following changes:
@@ -265,7 +268,13 @@ If your system has multiple network interfaces, use --dhcp-tag0=x, --dhcp-tag1=y
 
 This is described further on the Cobbler Wiki.
 
-=end
+=item --tags
+
+Assign a comma-delimited list of tags to the server.   Tags may contain whitespace inside, but leading/trailing whitespace will be stripped.   Tags may be used for per-system authorization and search/grouping in the CLI and webui.  See USER ACCESS CONTROL / AUTHORIZATION.
+
+=item virt-file-size
+
+=back
 
 =head2 ADDING A REPOSITORY TO MIRROR
 
@@ -346,6 +355,27 @@ Specifies what architecture the repository should use.  By default the current s
 
 =back
 
+=head2 ADDING A USER
+
+Users can be used to allow people access to some profiles/systems without giving them complete control over cobbler.  Of course, superusers have complete control.
+B<cobbler user add --name=string [--superuser=Y/N] [--tags=string]>
+
+=over
+
+=item name
+
+Just a normal day-to-day username.   It's probably good practice to follow similar constraints to system usernames, though cobbler doesn't force it.
+
+=item superuser
+
+Set a user to be a superuser.   This means complete access to everything without any checking.
+
+=item tags
+
+A list of tags that can be matched against systems/profiles for authorization.    They can also be used for list displays and searching.
+
+=back
+
 =head2 DISPLAYING CONFIGURATION ENTRIES
 
 The following commands are usable regardless of how you are using cobbler.
@@ -405,6 +435,16 @@ Sync should be run whenever files in /var/lib/cobbler are manually edited (which
 If using cobbler to manage a DHCP server (see the advanced section of this manpage), sync does need to be
 run after systems are added to regenerate and reload the DHCP configuration.
 
+=head2 USER ACCESS CONTROL / AUTHORIZATION
+
+If one of a user's tags appears in the tags of a system or its profile, the user is allowed to edit it.   The webui implements this using the REMOTE_USER environment variable, while the CLI implements it using the SUDO_USER variable when run under sudo.
+
+So, if a user has tags "hamsters, dogs", system1 has tags "dogs, cats", and system2 has tags "cats, mice", the user will be able to edit system1 and not sytem2.
+
+Systems always inherit tags from profiles/subprofiles, so if system2's profile has "hamsters" in its list, the user with "hamsters, dogs" will be able to edit it.
+
+To avoid all of this entirely and stick to the old behavior, simply set all users to be superusers.
+
 =head1 EXAMPLES
 
 =head2 IMPORT WORKFLOW
diff --git a/setup.py b/setup.py
index a780c21..3735a11 100644
--- a/setup.py
+++ b/setup.py
@@ -77,6 +77,7 @@ if __name__ == "__main__":
                                 (etcpath,  ['config/rsync.exclude']),
                                 (initpath, ['config/cobblerd']),
                                 (cobpath,  ['config/settings']),
+                                (cobpath,  ['config/users']),
 
                                 # backups for upgrades
                                 (backpath, []),
@@ -147,6 +148,8 @@ if __name__ == "__main__":
                                 (wwwtmpl,           ['webui_templates/system_edit.tmpl']),
                                 (wwwtmpl,           ['webui_templates/repo_list.tmpl']),
                                 (wwwtmpl,           ['webui_templates/repo_edit.tmpl']),
+                                (wwwtmpl,           ['webui_templates/user_list.tmpl']),
+                                (wwwtmpl,           ['webui_templates/user_edit.tmpl']),
 
                                 # Web UI common templates 
                                 (wwwtmpl,           ['webui_templates/paginate.tmpl']),
@@ -183,6 +186,8 @@ if __name__ == "__main__":
                                 ("%sadd/system/post" % trigpath,     []),
                                 ("%sadd/repo/pre" % trigpath,        []),
                                 ("%sadd/repo/post" % trigpath,       []),
+                                ("%sadd/user/pre" % trigpath,        []),
+                                ("%sadd/user/post" % trigpath,       []),
                                 ("%sdelete/distro/pre" % trigpath,   []),
                                 ("%sdelete/distro/post" % trigpath,  []),
                                 ("%sdelete/profile/pre" % trigpath,  []),
@@ -191,7 +196,8 @@ if __name__ == "__main__":
                                 ("%sdelete/system/post" % trigpath,  []),
                                 ("%sdelete/repo/pre" % trigpath,     []),
                                 ("%sdelete/repo/post" % trigpath,    []),
-                                ("%sdelete/repo/post" % trigpath,    []),
+                                ("%sdelete/user/pre" % trigpath,     []),
+                                ("%sdelete/user/post" % trigpath,    []),
                                 ("%ssync/pre" % trigpath,            []),
                                 ("%ssync/post" % trigpath,           [ "triggers/restart-services.trigger" ])
                              ],
diff --git a/webui_templates/master.tmpl b/webui_templates/master.tmpl
index abd09af..df66360 100644
--- a/webui_templates/master.tmpl
+++ b/webui_templates/master.tmpl
@@ -37,6 +37,7 @@
         <li><a href="$base_url/system_list" class="menu">Systems</a></li>
         <li><a href="$base_url/ksfile_list" class="menu">Kickstarts</a></li>
         <li><a href="$base_url/repo_list" class="menu">Repos</a></li>
+        <li><a href="$base_url/user_list" class="menu">Users</a></li>
         <li><hr/></li>
         <li>ADD</li>
         <li><a href="$base_url/distro_edit" class="menu">Distro</a></li>
@@ -44,6 +45,7 @@
         <li><a href="$base_url/subprofile_edit" class="menu">Subprofile</a></li>
         <li><a href="$base_url/system_edit" class="menu">System</a></li>
         <li><a href="$base_url/repo_edit" class="menu">Repo</a></li>
+        <li><a href="$base_url/user_edit" class="menu">User</a></li>
         <li><hr/><br/></li>
         <li><a class="button sync" href="$base_url/sync">Sync</a></li>
     </ul>
diff --git a/webui_templates/profile_edit.tmpl b/webui_templates/profile_edit.tmpl
index 44a7a82..5fc81ce 100644
--- a/webui_templates/profile_edit.tmpl
+++ b/webui_templates/profile_edit.tmpl
@@ -164,6 +164,21 @@ function disablename(value)
 
     <tr>
     <td>
+    <label for="tags">Tags</label>
+    </td>
+    <td>
+    <input type="text" size="128" style="width: 150px;" name="tags" id="tags"
+        #if $profile
+            #set $t = ', '.join( $profile.tags )
+            value="$t"
+        #end if
+    />
+    <p class="context-tip">A list of tags to associate with this profile.</p>
+    </td>
+    </tr>
+
+    <tr>
+    <td>
     <label for="virtfilesize">Virt Disk (GB)</label>
     </td>
     <td>
diff --git a/webui_templates/profile_list.tmpl b/webui_templates/profile_list.tmpl
index 047714a..5721f9a 100644
--- a/webui_templates/profile_list.tmpl
+++ b/webui_templates/profile_list.tmpl
@@ -14,6 +14,7 @@
         <tr>
             <th class="text">Name</th>
             <th class="text">Parent</th>
+            <th class="text">Tags</th>
             <th class="text">Kickstart</th>
         </tr>
     </thead>
@@ -26,6 +27,7 @@
             #set $tr_class = "rowodd"
         #end if
         #set $evenodd += 1
+        #set $tags = ', '.join($profile.tags)
 
         <tr class="$tr_class">
             <td>
@@ -38,6 +40,7 @@
                     <a href="$base_url/profile_edit?name=$profile.parent">$profile.parent</A>
                 #end if
             </td>
+            <td>$tags</td>
             <td>$profile.kickstart</td>
         </tr>
         #end for
diff --git a/webui_templates/system_edit.tmpl b/webui_templates/system_edit.tmpl
index 83d5908..36fd511 100644
--- a/webui_templates/system_edit.tmpl
+++ b/webui_templates/system_edit.tmpl
@@ -208,6 +208,20 @@ function page_onload() {
     </td>
     </tr>
 
+    <tr>
+    <td>
+    <label for="tags">Tags</label>
+    </td>
+    <td>
+    <input type="text" size="128" style="width: 150px;" name="tags" id="tags"
+        #if $system
+            #set $t = ', '.join( $system.tags )
+            value="$t"
+        #end if
+    />
+    <p class="context-tip">A list of tags to associate with this server.</p>
+    </td>
+    </tr>
 
     ## ====================================== start of looping through interfaces
 
diff --git a/webui_templates/system_list.tmpl b/webui_templates/system_list.tmpl
index 2e5833a..0820140 100644
--- a/webui_templates/system_list.tmpl
+++ b/webui_templates/system_list.tmpl
@@ -2,10 +2,12 @@
 
 #block body
 
+    #if $pages > 0
     ## ==== BEGIN PAGE NAVIGATION ====
     #set global what="system"
     #include "/usr/share/cobbler/webui_templates/paginate.tmpl"
     ## ==== END PAGE NAVIGATION ====
+    #end if
 
 <table class="sortable">
 
@@ -14,6 +16,7 @@
         <tr>
             <th class="text">Name</th>
             <th class="text">Profile</th>
+            <th class="text">Tags</th>
             ## FIXME: how to handle for multiple interface listing? <th class="text">MAC</th>
             ## <th class="text">IP</th>
             ## <th class="text">Hostname</th>
@@ -28,6 +31,7 @@
             #set $tr_class = "rowodd"
         #end if
         #set $evenodd += 1
+        #set $tags = ', '.join($system.tags)
 
         <tr class="$tr_class">
             <td>
@@ -36,6 +40,7 @@
             <td>
                 <a href="$base_url/profile_edit?name=${system.profile}">${system.profile}</a>
             </td>
+            <td>$tags</td>
             ## <td> ${system.mac_address} </td>
             ## <td> ${system.ip_address} </td>
             ## <td> ${system.hostname} </td>
diff --git a/webui_templates/user_edit.tmpl b/webui_templates/user_edit.tmpl
new file mode 100644
index 0000000..fc75509
--- /dev/null
+++ b/webui_templates/user_edit.tmpl
@@ -0,0 +1,99 @@
+#extends cobbler.webui.master
+#block body
+
+#if $user
+<script language="javascript">
+function disablename(value)
+{
+    document.getElementById("name").disabled=value;
+    if (value) {
+        document.getElementById("name").value = "$user.name";
+    }
+}
+</script>
+#end if
+
+<form method="post" action="$base_url/user_save">
+<fieldset id="cform">
+    #if $user
+        <legend>Editing User</legend>
+        <input type="hidden" name="new_or_edit" value="edit"/>
+        <input type="hidden" name="oldname" value="$user.name"/>
+    #else
+        <legend>Adding a User</legend>
+        <input type="hidden" name="new_or_edit" value="new"/>
+    #end if
+
+    <table border=0>
+
+    <tr>
+    <td>
+    <label for="name">User Name</label>
+    </td>
+    <td>
+    #if $user
+    <input type="text" size="128" style="width: 150px;" name="name" id="name" disabled="true"
+    #else
+    <input type="text" size="128" style="width: 150px;" name="name" id="name"
+    #end if
+        #if $user
+            value="$user.name"
+        #end if
+    />
+    <p class="context-tip">Example:  joe_smith</p>
+    </td>
+    </tr>
+
+    #if $user
+    <tr>
+    <td>
+    <label for="mode">Edit Mode</label>
+    </td>
+    <td>
+    <input type="radio" name="editmode" value="edit" checked onclick="javascript:disablename(true)">Edit
+    <input type="radio" name="editmode" value="rename" onclick="javascript:disablename(false)">Rename + Edit
+    <input type="radio" name="editmode" value="copy" onclick="javascript:disablename(false)">Copy + Edit
+    <p class="context-tip">How do you want to modify this object?</p>
+    </td>
+    </tr>
+    #end if
+
+    <tr>
+    <td>
+    <label for="tags">Tags</label>
+    </td>
+    <td>
+    <input type="text" size="128" style="width: 150px;" name="tags" id="tags"
+        #if $user
+            #set $t = ', '.join( $user.tags )
+            value="$t"
+        #end if
+    />
+    <p class="context-tip">A list of tags to associate with this user.</p>
+    </td>
+    </tr>
+
+    #if $user
+    <tr>
+    <td>
+    <label for="delete">Delete</label>
+    </td>
+    <td>
+       <input type="checkbox" name="delete1" value="delete1">Yes
+       <input type="checkbox" name="delete2" value="delete2">Really
+    <p class="context-tip">Check both buttons and click save to delete this object</p>
+    </td>
+    </tr>
+    #end if
+
+    <tr>
+    <td>
+    </td>
+    <td>
+    <input type="submit" name="submit" value="Save"/>
+    <input type="reset" name="reset" value="Reset"/>
+    </td>
+    </tr>
+
+</fieldset>
+#end block body
diff --git a/webui_templates/user_list.tmpl b/webui_templates/user_list.tmpl
new file mode 100644
index 0000000..bfd4462
--- /dev/null
+++ b/webui_templates/user_list.tmpl
@@ -0,0 +1,41 @@
+#extends cobbler.webui.master
+
+#block body
+
+    ## ==== BEGIN PAGE NAVIGATION ====
+    #set global what="user"
+    #include "/usr/share/cobbler/webui_templates/paginate.tmpl"
+    ## ==== END PAGE NAVIGATION ====
+
+
+<table class="sortable">
+    <thead>
+        <caption>Cobbler Users</caption>
+        <tr>
+            <th class="text">Name</th>
+            <th class="text">Tags</th>
+            <th class="text">Is Superuser?</th>
+        </tr>
+    </thead>
+    <tbody>
+        #set $evenodd = 1
+        #for $user in $users
+        #if $evenodd % 2 == 0
+            #set $tr_class = "roweven"
+        #else
+            #set $tr_class = "rowodd"
+        #end if
+        #set $evenodd += 1
+        #set $tags = ', '.join($user.tags)
+
+        <tr class="$tr_class">
+            <td>
+                <a href="$base_url/user_edit?name=$user.name">$user.name</a>
+            </td>
+            <td>$tags</td>
+            <td>$user.superuser</td>
+        </tr>
+        #end for
+    </tbody>
+</table>
+#end block body
-- 
1.5.3.4

_______________________________________________
et-mgmt-tools mailing list
et-mgmt-tools@xxxxxxxxxx
https://www.redhat.com/mailman/listinfo/et-mgmt-tools

[Index of Archives]     [Fedora Users]     [Fedora Legacy List]     [Fedora Maintainers]     [Fedora Desktop]     [Fedora SELinux]     [Big List of Linux Books]     [Yosemite News]     [KDE Users]     [Fedora Tools]

  Powered by Linux