TenantRoot and related models
diff --git a/xos/core/models/service.py b/xos/core/models/service.py
index 2f01972..ba3a991 100644
--- a/xos/core/models/service.py
+++ b/xos/core/models/service.py
@@ -139,23 +139,51 @@
     def delete(self, *args, **kwds):
         if not self.service.enabled:
             raise PermissionDenied, "Cannot modify permission(s) of a disabled service"
-        super(ServicePrivilege, self).delete(*args, **kwds)                    
-    
+        super(ServicePrivilege, self).delete(*args, **kwds)
+
     @staticmethod
     def select_by_user(user):
         if user.is_admin:
             qs = ServicePrivilege.objects.all()
         else:
             qs = SitePrivilege.objects.filter(user=user)
-        return qs        
+        return qs
+
+class TenantRoot(PlCoreBase):
+    """ A tenantRoot is one of the things that can sit at the root of a chain
+        of tenancy. This object represents a node.
+    """
+
+    KIND= "generic"
+    kind = StrippedCharField(max_length=30, default=KIND)
+
+    attribute = models.TextField(blank=True, null=True)
+
+    # helper for extracting things from a json-encoded attribute
+    def get_attribute(self, name, default=None):
+        if self.service_specific_attribute:
+            attributes = json.loads(self.attribute)
+        else:
+            attributes = {}
+        return attributes.get(name, default)
+
+    def set_attribute(self, name, value):
+        if self.service_specific_attribute:
+            attributes = json.loads(self.attribute)
+        else:
+            attributes = {}
+        attributes[name]=value
+        self.attribute = json.dumps(attributes)
 
 class Tenant(PlCoreBase):
     """ A tenant is a relationship between two entities, a subscriber and a
-        provider.
+        provider. This object represents an edge.
 
         The subscriber can be a User, a Service, or a Tenant.
 
         The provider is always a Service.
+
+        TODO: rename "Tenant" to "Tenancy"
     """
 
     CONNECTIVITY_CHOICES = (('public', 'Public'), ('private', 'Private'), ('na', 'Not Applicable'))
@@ -164,12 +192,20 @@
     KIND = "generic"
 
     kind = StrippedCharField(max_length=30, default=KIND)
-    provider_service = models.ForeignKey(Service, related_name='tenants')
-    subscriber_service = models.ForeignKey(Service, related_name='subscriptions', blank=True, null=True)
-    subscriber_tenant = models.ForeignKey("Tenant", related_name='subscriptions', blank=True, null=True)
-    subscriber_user = models.ForeignKey("User", related_name='subscriptions', blank=True, null=True)
+    provider_service = models.ForeignKey(Service, related_name='provided_tenants')
+
+    # The next four things are the various type of objects that can be subscribers of this Tenancy
+    # relationship. One and only one can be used at a time.
+    subscriber_service = models.ForeignKey(Service, related_name='subscribed_tenants', blank=True, null=True)
+    subscriber_tenant = models.ForeignKey("Tenant", related_name='subscribed_tenants', blank=True, null=True)
+    subscriber_user = models.ForeignKey("User", related_name='subscribed_tenants', blank=True, null=True)
+    subscriber_root = models.ForeignKey("TenantRoot", related_name="subscribed_tenants", blank=True, null=True)
+
+    # Service_specific_attribute and service_specific_id are opaque to XOS
     service_specific_id = StrippedCharField(max_length=30, blank=True, null=True)
     service_specific_attribute = models.TextField(blank=True, null=True)
+
+    # Connect_method is only used by Coarse tenants
     connect_method = models.CharField(null=False, blank=False, max_length=30, choices=CONNECTIVITY_CHOICES, default="na")
 
     def __init__(self, *args, **kwargs):
@@ -203,27 +239,6 @@
             attributes = {}
         return attributes.get(name, default)
 
-    def update_attribute_from_initial(self):
-        # XXX not sure I want to pursue this approach...
-        try:
-            attributes = json.loads(self._initial["service_specific_attribute"])
-        except:
-            attributes = {}
-
-        if not self.service_specific_attribute:
-            # the easy case -- nothing has changed, so keep the original
-            # attribute
-            self.service_specific_attribute = json.dumps(orig_attributes)
-            return
-
-        try:
-            new_attributes = json.loads(self.service_specific_attribute)
-        except:
-            raise XOSValidationError("Unable to parse service_specific_attribute")
-
-        attributes.update(new_attributes)
-        self.service_specific_attribute = json.dumps(attributes)
-
     @classmethod
     def get_tenant_objects(cls):
         return cls.objects.filter(kind = cls.KIND)
@@ -243,6 +258,7 @@
                 raise XOSDuplicateKey("service_specific_id %s already exists" % self.service_specific_id, fields={"service_specific_id": "duplicate key"})
 
 class CoarseTenant(Tenant):
+    """ TODO: rename "CoarseTenant" --> "StaticTenant" """
     class Meta:
         proxy = True
 
@@ -255,3 +271,53 @@
             raise XOSValidationError("subscriber_tenant and subscriber_user must be null")
 
         super(CoarseTenant,self).save()
+
+class Subscriber(TenantRoot):
+    """ Intermediate class for TenantRoots that are to be Subscribers """
+
+    class Meta:
+        proxy = True
+
+    KIND = "Subscriber"
+
+class Provider(TenantRoot):
+    """ Intermediate class for TenantRoots that are to be Providers """
+
+    class Meta:
+        proxy = True
+
+    KIND = "Provider"
+
+class TenantRootRole(PlCoreBase):
+    ROLE_CHOICES = (('admin','Admin'),)
+
+    role = StrippedCharField(choices=ROLE_CHOICES, unique=True, max_length=30)
+
+    def __unicode__(self):  return u'%s' % (self.role)
+
+class TenantRootPrivilege(PlCoreBase):
+    user = models.ForeignKey('User', related_name="tenant_root_privileges")
+    tenant_root = models.ForeignKey('TenantRoot', related_name="tenant_root_privileges")
+    role = models.ForeignKey('TenantRootRole', related_name="tenant_root_privileges")
+
+    class Meta:
+        unique_together = ('user', 'tenant_root', 'role')
+
+    def __unicode__(self):  return u'%s %s %s' % (self.slice, self.user, self.role)
+
+    def save(self, *args, **kwds):
+        if not self.user.is_active:
+            raise PermissionDenied, "Cannot modify role(s) of a disabled user"
+        super(SlicePrivilege, self).save(*args, **kwds)
+
+    def can_update(self, user):
+        return user.can_update_tenant_root(self.tenant_root)
+
+    @staticmethod
+    def select_by_user(user):
+        if user.is_admin:
+            qs = TenantRoot.objects.all()
+        else:
+            sp_ids = [sp.id for sp in TenantRoot.objects.filter(user=user)]
+            qs = TenantRoot.objects.filter(id__in=sp_ids)
+        return qs
diff --git a/xos/core/models/user.py b/xos/core/models/user.py
index 4f8cfa9..91672c5 100644
--- a/xos/core/models/user.py
+++ b/xos/core/models/user.py
@@ -324,7 +324,17 @@
         if ServicePrivilege.objects.filter(
             service=service, user=self, role__role__in=['admin', 'Admin']+allow):
             return True
-        return False           
+        return False
+
+    def can_update_tenant_root(self, tenant_root):
+        from core.models.service import TenantRoot
+        from core.models.site import SitePrivilege
+        if self.can_update_root():
+            return True
+        if TenantRootPrivilege.objects.filter(
+            tenant_root=tenant_root, user=self, role__role__in=['admin', 'Admin']+allow):
+            return True
+        return False
 
     @staticmethod
     def select_by_user(user):