Segregated multi-site support #7521
Replies: 20 comments 1 reply
-
Looks like Django sites framework's CurrentSiteManager might be useful. Perhaps the requested functionality could simply be implemented as an option in Settings. |
Beta Was this translation helpful? Give feedback.
-
Hi @DanAtShenTech - thanks for the suggestion. This is indeed something we're keen to implement, and there are various strands of work going towards this (#2869, #4206, #4207) - although it looks like we don't currently have an open issue for the feature as a whole since #2463 was closed, so I'll leave this one open. It's possible to achieve most of this through user groups and permissions - in particular, a user who's been assigned permission over a single sub-site won't see other sites in the explorer. The main thing that's missing at the moment is introducing a 'choose' permission level so that we can restrict the pages shown in the 'choose a page' popup. Note that we want to avoid tying our model of limited-access users too closely to the concept of 'sites', since we want to keep things flexible enough to allow for other arrangements - for example, an organisation might want to enforce this kind of compartmentalisation between sections of a site, without having a distinct domain name for each one. |
Beta Was this translation helpful? Give feedback.
-
Could you elaborate on this? I'm having trouble finding any clear avenues for assigning permissions to a site. Thank you! :) ( +1 to fleshing this feature out further, also 👍 ) |
Beta Was this translation helpful? Give feedback.
-
My friend and I hacked together something that sort of worked: https://github.com/mjlabe/bwagtail-cms We ended up filtering via SQL: https://github.com/mjlabe/bwagtail-cms/blob/master/bwagtail/bwagtail/get_user_sites.py And overriding the views for switching the site: https://github.com/mjlabe/bwagtail-cms/blob/master/bwagtail/wag_custom/views.py https://github.com/mjlabe/bwagtail-cms/blob/master/bwagtail/wag_custom/forms.py |
Beta Was this translation helpful? Give feedback.
-
This video might be useful in implementing this feature. |
Beta Was this translation helpful? Give feedback.
-
I ran a bit farther with @mjlabe's implementation, and made a few small updates:
Not production ready, but if this is a helpful stepping stone in getting someone else toward a workable prototype, here you go: https://gist.github.com/bahoo/5f073c15d0e3ff742ab14ecc5a671de9 |
Beta Was this translation helpful? Give feedback.
-
While the tree-based permissions of the page tree and collections allow a great deal of flexibility in defining how multi-tenancy should work for a project - I can't help bit think we're missing something to pull it all together. I would like to propose a new Like we did for images and documents when Collections were introduced, we could create a single All views and choosers would use the current We'd probably also need to provide a version of A few other things that would need thinking about are:
Interested to hear what folks think about this general approach? @mjlabe @bahoo @DanielSwain Does this sound like something that would fit your use-cases? If feedback is good, I'll work up these ramblings into a proper RFC. |
Beta Was this translation helpful? Give feedback.
-
It sounds great to me. I think it would be best to have dropdowns to assign
a site to a CMS instance. Even if Django admin were left as is, I still
think it would work for me. The Django admin site is for true admins with
access to everything and the wagtail admin would work on a per CMSInstance
basis.
…On Sun, Nov 24, 2019, 6:57 PM Andy Babic ***@***.***> wrote:
While the tree-based permissions of the page tree and collections allow a
great deal of flexibility in defining how multi-tenancy should work for a
project - I can't help bit think we're missing something to pull it all
together.
I would like to propose a new CMSInstance model to fulfil this role -
which would represent an 'instance of the CMS' accessed by editors (as
opposed to a Site which is accessed by front-end users). And like a Site,
a CMSInstance would be identifiable by the domain of the current request,
but it would only apply to the CMS (which might be at an entirely different
domain to any of the Site objects).
Like we did for images and documents when Collections were introduced, we
could create a single CMSInstance by default and associate all Site
objects with it (via a new ForeignKey field on the Site model), and
things like the site switcher in wagtail.contrib.settings (and perhaps
the site list itself) would be limited to sites associated with the current
CMSInstance.
All views and choosers would use the current CMSInstance to filter out
anything from other instances, and automatically associate content with the
correct instance. And, like we do with wagtail.search.models.Indexed, we
could include a model mixin (e.g. CMSInstanceSpecific) that developers
could use to create models with content unique to each instance.
We'd probably also need to provide a version of CMSInstanceSpecific to
help create tenant-specific user models, (username would have to be unique
per CMSInstance, rather than completely unique), which would be important
for managing users / configuring admin permissions on a per-instance basis
(admin authentication would need a rejig too).
A few other things that would need thinking about are:
- What the relationship between CMSInstance / Collection / Image /
Document should be
- What we'd do about tags (each CMSInstance should probably have it's
own tag set/cloud)\
- How would non-CMS users be associated with a CMSInstance - maybe
indirectly via Site?
- Whether there would be a UI to manage this, or whether (like
Collections), developers would manage CMSInstance objects outside of
Wagtail.
Interested to hear what folks think about this general approach? @mjlabe
<https://github.com/mjlabe> @bahoo <https://github.com/bahoo> @DanielSwain
<https://github.com/DanielSwain> Does this sound like something that
would fit your use-cases? If feedback is good, I'll work up these ramblings
into a proper RFC.
—
You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub
<#4288?email_source=notifications&email_token=AG3DIIIDTQYYBPQND7WRW6LQVMIE3A5CNFSM4EQXZWH2YY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEFAYQ5I#issuecomment-557942901>,
or unsubscribe
<https://github.com/notifications/unsubscribe-auth/AG3DIIPPKXJ4AX24447IBS3QVMIE3ANCNFSM4EQXZWHQ>
.
|
Beta Was this translation helpful? Give feedback.
-
To add to this discussion, I am including here references to my two comments from July 9, 2019. I also plan to add some other thoughts here as soon as I have time to write them up. |
Beta Was this translation helpful? Give feedback.
-
@ababic did your proposal ever make it into an RFC (I looked [here](https://github.com/wagtail/rfcs/pulls and couldn't find it)? My group (in a clinical informatics lab in Boston) are just (i.e. the past 2 weeks) getting started with Wagtail to deal with a sudden surge of content related to the pandemic across our network of hospitals. Loving it so far, but the enhancement discussed here sounds like it might be ideal for our use case now and longer term. In our case it's not different Chambers of Commerce (the example used by @DanielSwain above), but different hospitals, academic centers, and other organizational units that we support that may have similar content, needs, but need to be segregated in the admin UI. Would love to contribute to the discussion around this and pick a direction for our urgent need that is compatible with community plans to address this. |
Beta Was this translation helpful? Give feedback.
-
Hi @carlb. Thanks for posting! It just so happens I implemented something like the above for a client project recently. It made a few things clearer in my mind, but many elements of the solution were probably a bit too bespoke to apply to many projects (otherwise I would have open-sourced it). I haven't put together an RFC yet, as the issue is so multi-facetted that it probably demands 3 or 4 RFCs to tackle the various different aspects, and I simply haven't had the time to put them together (although I'd certainly still like to). The
Making things completely segregated is really quite a challenge, I've found. If you want true segregation between each site, I would highly recommend investing in infrastructure instead, and simply host the same codebase multiple times (especially if free hosting plans will do, and the sites are well-suited to upstream caching with something like Cloudflare). Having separate instances makes environment configuration and resource usage monitoring easier, and not depending on a single environment will make things more resilient overall. If you do need to have cross-talk between sites (or some kind of 'overview'), it should be possible to implement REST/Graph API endpoints to achieve that. And if you're concerned about users having to manage accounts/passwords for multiple sites, a basic SSO implementation can help with that. |
Beta Was this translation helpful? Give feedback.
-
I am still very hopeful to see this feature come to pass. In preparation for this eventuality, I want to mention my comment here about use of a |
Beta Was this translation helpful? Give feedback.
-
@DanielSwain Broadly speaking, I would say the remaining pieces of the puzzle are:
Even with all these things in place, I think Wagtail might still have to recommend separate hosting as the favoured way to segregate sites (Where the number is manageable... e.g. approx 15 or fewer), not least because all it takes is for someone to make the Django admin area available (or some other Django app that doesn't care about Wagtail's site-specific view logic), and you immediately have important security and privacy implications. |
Beta Was this translation helpful? Give feedback.
-
To handle some restrictions I created the folowing models. class ProxySite(Site):
objects = ProxySiteManager() < see below
class Meta:
proxy = True
@property
def home_page(self):
return self.root_page
class RootPage(Page): < toplevel
parent_page_types = []
subpage_types = ['cms.home_page')]
is_movable = False
is_copyable = False
is_deletable = False
is_unpublishable = False
@property
def can_add_homepage(self):
return self.get_children().count() < MAX_SITES
class HomePage(Page):
max_count = MAX_SITES
is_movable = False
is_unpublishable = False
@property
def is_copyable(self):
return self.get_parent().specific.can_add_homepage
@property
def is_deletable(self):
return self.get_parent().get_children().count() > 1
subpage_types = [
'cms.search_page',
'cms.default_page',
'cms.album_page',
'cms.news_page'
]
class DefaultPage(Page):
parent_page_types = [
'cms.home_page',
'cms.default_page'
]
subpage_types = [
'cms.default_page',
]
class ProxySiteQuerySet(models.query.QuerySet):
def default(self):
try:
return self.get(is_default_site=True)
except:
return
def not_default(self):
return self.filter(is_default_site=False)
def default_or_first(self):
return self.default() or self.first()
def get_user_sites(self, user):
user_perms = UserPagePermissionsProxy(user)
if user_perms.can_edit_pages():
site_pks = list(page.get_site().pk for page in user_perms.editable_pages().all())
site_pks = list(sorted(set(site_pks)))
return self.filter(pk__in=site_pks).distinct().all()
return self.none()
def get_current_user_site(self, user, site):
user_site = self.none()
user_sites = self.get_user_sites(user)
try:
user_site = user_sites.get(hostname=site.hostname)
except ObjectDoesNotExist:
user_site = user_sites.first()
return user_site
def get_sites_for_hostname(self, hostname, port):
return self.annotate(
match = Case(
When(hostname = hostname, port = port, then = MATCH_HOSTNAME_PORT),
When(hostname = hostname, is_default_site = True, then = MATCH_HOSTNAME_DEFAULT),
When(is_default_site = True, then = MATCH_DEFAULT),
default=MATCH_HOSTNAME,
output_field=IntegerField()
)
).filter(
Q(hostname=hostname) | Q(is_default_site=True)
).order_by(
'match'
).select_related(
'root_page'
)
class ProxySiteManager(models.Manager):
""" Is in wagtail’s SiteManager """
def get_by_natural_key(self, hostname, port):
return self.get(hostname=hostname, port=port)
def get_queryset(self):
return ProxySiteQuerySet(self.model, using=self._db)
def default(self):
return self.get_queryset().default()
def not_default(self):
return self.get_queryset().not_default()
def default_or_first(self):
return self.get_queryset().default_or_first()
def get_user_sites(self, user):
return self.get_queryset().get_user_sites(user)
def get_current_user_site(self, user, site):
return self.get_queryset().get_current_user_site(user, site)
def get_sites_for_hostname(self, hostname, port):
return self.get_queryset().get_sites_for_hostname(hostname, port) Editors are linked to one or more home page using group and permissions. And some hooks prevent unwanted actions. @hooks.register('before_delete_page')
def before_delete_home_page(request, page):
if request.method == 'POST':
return
home_page = request.proxy_site.home_page.specific
specific_page = page.specific
if specific_page.__class__ == home_page.__class__:
if request.user.is_superuser:
messages.warning(
request,
_('This is the {home_page} of {site_name}. When deleted the site will be removed to.').format(
home_page = home_page._meta.verbose_name.lower(),
site_name = request.proxy_site.site_name
)
)
else:
messages.error(
request,
_('You are not allowed to delete the {home_page}.').format(
home_page = home_page._meta.verbose_name.lower()
)
)
return redirect('wagtailadmin_explore', page.id)
@hooks.register('before_copy_page')
def before_copy_home_page(request, page):
home_page = request.proxy_site.home_page.specific
specific_page = page.specific
if specific_page.__class__ == home_page.__class__:
if request.user.is_superuser:
root_page = home_page.get_parent().specific
if root_page.can_add_homepage:
messages.warning(
request,
_('Please note that you are about to copy the {home_page}.').format(
home_page = home_page._meta.verbose_name.lower()
)
)
else:
messages.error(
request,
_('The root page doesn’t allow you to copy the {home_page}.').format(
home_page = home_page._meta.verbose_name.lower()
)
)
return redirect('wagtailadmin_explore', page.id)
else:
messages.error(
request,
_('You are not allowed to copy the {home_page}.').format(
home_page = home_page._meta.verbose_name.lower()
)
)
return redirect('wagtailadmin_explore', page.id) |
Beta Was this translation helpful? Give feedback.
-
Unfortunately the above code shows an errormessage in the navigator due to 404 for the rootpage raised by the api:
|
Beta Was this translation helpful? Give feedback.
-
FWIW / if it's helpful — folx on Postgres, I've had some success with Django Tenants, until ( / if ) Wagtail does its own in-house so to speak. https://github.com/django-tenants/django-tenants |
Beta Was this translation helpful? Give feedback.
-
FWIW, I would echo Matt's comments above by example with my current project's use case: Having discrete "sites" but not discrete users is incidentally very good for my present use case. I'm building a project for media distribution, and one of the benefits of Wagtail's extra layer on top of Django is the ability to easily implement syndication across multiple sites (in that a user signed up for one site is signed up for all sites, and third party payment models like Stripe built on top of user permissions for multiple tiers of content across multiple sites "just work"). Not every thing under the sun is a site hosting platform intended to provide discrete websites for discrete users. And for those where that is the use case, what can be coded in Python that's easier than deploying another container in the same pool of servers and another postgres schema in the same database deployment/instance? I assure you that dockerfiles and ansible scripts are easier to write than extending the functionality of a framework ;). |
Beta Was this translation helpful? Give feedback.
-
I agree with @awhileback ... The general idea with multi-site is that the various sites are going to share users, data, templates, code, etc. So trying to segregate them seems to go against that idea. It sounds like the OP is trying to build a product, specifically for hosting his customers (chambers of commerce). We also went down this road early on, and realized that mutli-site anything (Wagtail, WordPress, etc.) is the wrong approach (at least for our needs). Something of that magnitude is going to require a lot of custom development. The more ideal use-case that comes to mind is use of Wagtail in a corporate environment where there may be separate brands used in a multi-site (e.g. MegaCorp has subsidiaries SmallerCorp and OtherCorp - both of which share similar website designs). In that case an employee of SmallerCorp should have some segregation from ability to edit OtherCorp, which the Wagtail permissions seem to already do a good job of. In this scenario SmallerCorp should not be able to delete/edit media, pages, etc. from OtherCorp, but I still have not seen any real-world example where they need total isolation between them. Any organization that needs total isolation between sites almost always ends up with totally different features, designs, etc. between the sites anyhow, and is happy to have two separate codebases (because at that point the developers may also need to be isolated!). |
Beta Was this translation helpful? Give feedback.
-
We have definitely had people try to push us to add features or make design changes to accommodate their preferences. And in some instances we have caved to pressure and added features that we place behind feature flags so we can offer them on only one or two sites. But in general Caltech has been able to hold the line on only adding features that can be offered on all sites. This is mainly because "it is free" is a powerful incentive for academics. And we are able to offer sites for free because the marginal cost of a new site in a multitenant system is close to zero. I would like to find time to revisit Andy's list in this discussion and make some RFC proposals. But in the mean time, I have written up the patches we have implemented to enforce multitenancy. |
Beta Was this translation helpful? Give feedback.
-
I spent some time 'a year or so' ago doing a more thorough write-up about how true/full Multitenancy might work in Wagtail, and some more on a POC to get a feel for the code changes that would be required to support my ideas. Upon presenting the changes to the core team, we concluded that, ultimately, the number of practical use-cases for native multitenancy are so few that the added project complexity for such an approach would be too burdensome, and not something they would be willing to persue. |
Beta Was this translation helpful? Give feedback.
-
The current multi-site support in Wagtail assumes that all sites are for the same organization. If I wanted to create sites for multiple organizations of the same type (for instance, Chambers of Commerce) on a single Wagtail installation, then the images and documents uploaded by one organization are visible to all. Also, the pages of all organizations are visible to everyone else in the Wagtail Explorer. Creating multiple sites for organizations of the same type on a single Wagtail installation would provide the capability to view statistics across multiple organizations and to promote cross-organization communication among members (and probably other interesting things as well). Please consider implementing segregated multi-site support in which users who are assigned editing permissions per site can only view the pages and assets of that site.
Beta Was this translation helpful? Give feedback.
All reactions