How do I disable local role acquisition after a workflow transition? - plone

Using the Intranet workflow, and when the user selects the hide transition to move the content to the private state, I want to disable local role acquisition. I think I'll want to re-enable when they move it back to internal draft.
I can see how this is done by the sharing view within plone.app.workflow. It has a method, update_inherit, that sets context.__ac_local_roles_block__ to either True or None.
How might I do something similar with a transition after_script?
Plone 4.1.5
plone.app.workflow 2.0.6
More info on my use case:
We have two users, contributor_1 and contributor_2, possessing only the Member role globally. At the site root, create a folder and assign the two users local roles to add, edit, and view.
Here is the default private state/manage_permissions:
If contributor_1 creates a page and tries to hide it, contributor_2 can still view, edit, and transition it back to internal draft because contributor_2 is inheriting those local roles.
Here is what I first tried:
While this does effectively make the page private to only contributor_1 and any Manager, contributor_1 cannot share access with contributor_2 while the item is in the private state. No private collaboration. UPDATE: contributor_1 can manipulate the sharing view (ie. they have access to delegate local roles), but delegating those local roles (Contributor, Editor, Reader) does not translate to their respective access since I have revoked the role-permission mapping in the above workflow.
Now, if set back to the default permission map, users can achieve the private state they want by hiding the content item and then going to the sharing tab and disabling local role acquisition. The problem is they don't want to have to remember to click the checkbox on the sharing tab every time they want to hide something.

As SteveM already commented, the workflow tool can handle this for you.
You need need to manage all the necessary permission with your workflow, this depends on your needs.
UPDATE: You workflow has to manage the "Delegate Roles XYZ [Reader/Contributor/etc.]" and "Change local roles" permissions. This way the user "contributor_1" can delegate roles in the "Private" area.
Do NOT acquire any permission settings on your hidden state.
In your case the Owner (probably the manager too) needs some permissions to view/edit/changestate.
Another hint for defining workflows.
Check this package it generally manages all the permissions in action groups, like View (View, Access content permissions, etc) or Add (Add Folders, Add portal content, Add..., etc.) All Plone default permissions are mapped to the right action group, and it let's you create a workflow within 5 minutes. The best part is, you can write the workflow in human readable specification file (custom DSL).

I created a utils.py module within my package containing one method: a (nearly) straight copypasta of the update_inherit method the sharing view uses. Then, I imported utils in my package's __init__.py.
from AccessControl import Unauthorized
from AccessControl.SecurityInfo import ModuleSecurityInfo
from Acquisition import aq_base
from Products.CMFCore import permissions
from Products.CMFCore.utils import getToolByName
security = ModuleSecurityInfo( 'united.policy.utils' )
security.declarePublic('update_inherit')
def update_inherit(context, status=True, reindex=True):
"""Enable or disable local role acquisition on the context.
Returns True if changes were made, or False if the new settings are the
same as the existing settings.
"""
portal_membership = getToolByName(context, 'portal_membership')
if not portal_membership.checkPermission(permissions.ModifyPortalContent,
context):
raise Unauthorized
block = not status
oldblock = bool(getattr(aq_base(context), '__ac_local_roles_block__',
False))
if block == oldblock:
return False
if block:
# If user has inherited local roles and removes inheritance, locally
# set roles he inherited before to avoid definitive loss of access
# (refs #11945)
user = portal_membership.getAuthenticatedMember()
context_roles = user.getRolesInContext(context)
global_roles = user.getRoles()
local_roles = [r for r in context_roles if r not in global_roles]
if local_roles:
context.manage_setLocalRoles(user.getId(), local_roles)
context.__ac_local_roles_block__ = block and True or None
if reindex:
context.reindexObjectSecurity()
return True
Then, I created two scripts within my ./profiles/default/workflows/intranet_workflow/scripts/ directory bound to their respective transitions:
after_hide.py
## Script (Python) "after_hide"
##bind container=container
##bind context=context
##bind namespace=
##bind script=script
##bind subpath=traverse_subpath
##parameters=review_state
##title=
##
from united.policy.utils import update_inherit
update_inherit(review_state.object, False)
after_show.py
## Script (Python) "after_show"
##bind container=container
##bind context=context
##bind namespace=
##bind script=script
##bind subpath=traverse_subpath
##parameters=review_state
##title=
##
from united.policy.utils import update_inherit
update_inherit(review_state.object, True)

Related

Pages 404 when created via the django-cms API

I'm building a system which creates a site object for clients added to the system and off the back of that creates some basic pages for the site.
Pages are created with published=True however after browsing to them via pages admin or directly to URLs the result is a 404.
To fix the 404 I've noticed simply saving the page's settings then makes the pages load. I therefore suspect there's further actions which are triggered by the CMS admin, whether that be through the form used or signals.
The Code
The creation of pages comes from a post_save signal;
#receiver(post_save, sender=Client)
def create_site_on_client_creation(instance, **kwargs):
"""
When a Client is created, create a Site object with the client slug as a
sub-domain to the main sites base domain. Also update the site object
if the client slug changes.
"""
site = instance.site or Site()
if not instance.site_id:
instance.site = site
instance.save(update_fields=['site'])
create_initial_pages(
site, extra_pages=[
'Privacy Policy',
'Terms of Use'
]
)
The function then creating the content is;
def create_initial_pages(site, **kwargs):
""" Create the initial pages required for a site """
# Create a CMS "home" page on the initial site creation
created_root = False
root_page = Page.objects.on_site(site).drafts().filter(
title_set__language=settings.LANGUAGE_CODE,
title_set__title=HOME_TITLE,
is_home=True
).first()
if not root_page:
created_root = True
root_page = create_page(
title=HOME_TITLE,
template=TEMPLATE_INHERITANCE_MAGIC,
language=settings.LANGUAGE_CODE,
site=site,
published=True
)
if 'extra_pages' in kwargs:
for page_title in kwargs['extra_pages']:
# Check if we can find
existing = Page.objects.on_site(site).drafts().filter(
title_set__language=settings.LANGUAGE_CODE,
title_set__title=page_title,
).exists()
if not existing:
create_page(
title=page_title,
template=TEMPLATE_INHERITANCE_MAGIC,
language=settings.LANGUAGE_CODE,
site=site,
parent=root_page,
published=True
)
if created_root:
root_page.is_home = True
root_page.save()
I think what's happening here is that you are making changes to the root page after it is published which is therefore making changes to the draft:
if created_root:
root_page.is_home = True
root_page.save()
# You may need to publish here
root_page.publish(settings.LANGUAGE_CODE)
The API always fires the publish action on creation if the published kwarg is added: https://github.com/divio/django-cms/blob/develop/cms/api.py#L203
The tests are a good place to see how the api is used by code, finding a test that creates a page tree should help you to see if you have missed a specific setting that causes your page to not be seen: https://github.com/divio/django-cms/blob/develop/cms/tests/test_multilingual.py#L85
Also be sure that your new site has a language configuration in the settings file, it's possible that the admin is confused by the page and language combination and when you "save the page settings" you are actually creating an entry in your current language: http://docs.django-cms.org/en/latest/reference/configuration.html#internationalisation-and-localisation-i18n-and-l10n
I would advise rolling / commenting your code back to just the one root page and get that working as you expect, then when that is working you can expand upon it with more pages.
Try just calling the publish method (with a language) on the page after the call to create_page. You'll need to get the page from the create_page method. e.g...
my_page = create_page(
title=page_title,
template=TEMPLATE_INHERITANCE_MAGIC,
language=settings.LANGUAGE_CODE,
site=site,
parent=root_page,
published=True
)
my_page.publish("en")

Adding a button for google signin using f#/fable/asp.net/react

I'm working with the SAFE stack (https://safe-stack.github.io/) and through the example dojo. It's great so far.
I'd like to extend the example to include a button to login/auth via Google. So I looked at an example on the Google website (https://developers.google.com/identity/sign-in/web/build-button). And then I had a look how to do authentication using ASP.NET (https://learn.microsoft.com/en-us/aspnet/core/security/authentication/social/google-logins?view=aspnetcore-2.1&tabs=aspnetcore2x) As a result I ended up confused as to how to integrate this into a SAFE project. Can someone tell me what they would do? SHould I be trying to use ASP.NET Identity or should I be using the JWT approach? I don't even know if they are the same since I'm very new to web frameworks.....
The other question I have is how would one inject raw Javascript into the client side of a SAFE project. The google example above shows raw JS/CSS/HTML code? Should I be injecting that as is or should I look in React for some button that does this and map that idea back through Fable?
Setting up OAuth
The easiest way to use Google OAuth is to wait until the next release of Saturn, at which point Saturn will include the use_google_oauth feature that I just added. :-) See the source code if you're interested in how it works, though I'm afraid you can't implement this yourself with use_custom_oauth because you'll run into a type error (the underlying ASP.NET code has a GoogleOptions class, and use_custom_oauth wants an OAuthOptions class, and they aren't compatible).
To use it, add the following to your application CE:
use_google_oauth googleClientId googleClientSecret "/oauth_callback_google" []
The last parameter should be a sequence of string * string pairs that represent keys and values: you could use a list of tuples, or a Map passed through Map.toSeq, or whatever. The keys of that sequence are keys in the JSON structure that Google returns for the "get more details about this person" API call, and the values are the claim types that those keys should be mapped to in ASP.NET's claims system. The default mapping that use_google_oauth already does is:
id → ClaimTypes.NameIdentifier
displayName → ClaimTypes.Name
emails[] (see note) → ClaimTypes.Email
Those three are automatically mapped by ASP.NET. I added a fourth mapping:
avatar.url → `"urn:google:avatar:url"
There's no standard ClaimTypes name for this one, so I picked an arbitrary URN. Caution: this feature hasn't been released yet, and it's possible (though unlikely) that this string might change between now and when the feature is released in the next version of Saturn.
With those four claim types mapped automatically, I found that I didn't need to specify any additional claims, so I left the final parameter to use_google_oauth as an empty list in my demo app. But if you want more (say you want to get the user's preferred language to use in your localization) then just add them to that list, e.g.:
use_google_oauth googleClientId googleClientSecret "/oauth_callback_google" ["language", "urn:google:language"]
And then once someone has logged in, look in the User.Claims seq for a claim of type "urn:google:language".
Note re: the emails[] list in the JSON: I haven't tested this with a Google account that has multiple emails, so I don't know how ASP.NET picks an email to put in the ClaimTypes.Email claim. It might just pick the first email in the list, or it might pick the one with a type of account; I just don't know. Some experimentation might be needed.
Also note that third-party OAuth, including GitHub and Google, has been split into a new Saturn.Extensions.Authorization package. It will be released on NuGet at the same time that Saturn's next version (probably 0.7.0) is released.
Making the button
Once you have the use_google_oauth call in your application, create something like the following:
let googleUserIdForRmunn = "106310971773596475579"
let matchUpUsers : HttpHandler = fun next ctx ->
// A real implementation would match up user identities with something stored in a database, not hardcoded in Users.fs like this example
let isRmunn =
ctx.User.Claims |> Seq.exists (fun claim ->
claim.Issuer = "Google" && claim.Type = ClaimTypes.NameIdentifier && claim.Value = googleUserIdForRmunn)
if isRmunn then
printfn "User rmunn is an admin of this demo app, adding admin role to user claims"
ctx.User.AddIdentity(new ClaimsIdentity([Claim(ClaimTypes.Role, "Admin", ClaimValueTypes.String, "MyApplication")]))
next ctx
let loggedIn = pipeline {
requires_authentication (Giraffe.Auth.challenge "Google")
plug matchUpUsers
}
let isAdmin = pipeline {
plug loggedIn
requires_role "Admin" (RequestErrors.forbidden (text "Must be admin"))
}
And now in your scope (NOTE: "scope" will probably be renamed to "router" in Saturn 0.7.0), do something like this:
let loggedInView = scope {
pipe_through loggedIn
get "/" (htmlView Index.layout)
get "/index.html" (redirectTo false "/")
get "/default.html" (redirectTo false "/")
get "/admin" (isAdmin >=> htmlView AdminPage.layout)
}
And finally, let your main router have a URL that passes things to the loggedInView router:
let browserRouter = scope {
not_found_handler (htmlView NotFound.layout) //Use the default 404 webpage
pipe_through browser //Use the default browser pipeline
forward "" defaultView //Use the default view
forward "/members-only" loggedInView
}
Then your login button can just go to the /members-only route and you'll be fine.
Note that if you want multiple OAuth buttons (Google, GitHub, Facebook, etc) you'll probably need to tweak that a bit, but this answer is long enough already. When you get to the point of wanting multiple OAuth buttons, go ahead and ask another question.

SetPermission With sensenet API - version 6.3

I am working on sensenet API. I faced an issue with setPermission on sensenetAPI security.
As per concern, when I create a document I would like to give See, open, Save and RunApplication permission as a default for newly created document to the user(User is taken from the function parameter).
To achieve this I use below code
public static void SetCollabUserSecurity(string myUserEmailId, Node myNodetToSetSecurity)
{
var domainName = "Builtin";
var strUsername = GetNameFromEmail(myUserEmailId);
User user;
using (new SystemAccount())
{
user = User.Load(domainName, strUsername);
if (user != null && user.Enabled)
{
var myUser = user;
myNodetToSetSecurity.Security.SetPermission(myUser, true, PermissionType.See,
PermissionValue.Allow);
myNodetToSetSecurity.Security.SetPermission(myUser, true, PermissionType.Open,
PermissionValue.Allow);
myNodetToSetSecurity.Security.SetPermission(myUser, true, PermissionType.Save,
PermissionValue.Allow);
myNodetToSetSecurity.Security.SetPermission(myUser, true, PermissionType.RunApplication,
PermissionValue.Allow);
}
}
}
While I am using this function, my process for creating document becomes time consuming. It takes around 40 second time for execution.
So in case of, if I would like to share the same newly created document with multiple users, lets say there are 3 user and I want to give the above permission to all of them then my single function call takes 120 second (2 minute) time to simply assign permission.
Is there any Odata REST API call available or any sensenet library call available through which I can assign...
1) multiple permission to multiple user for single document or
2) multiple permission to single user for single document
Can anyone help to come out from this issue?
Thanks!
C# api
On the server there is a c# api for managing permissions, please check this article for details. You may use the AclEditor class for setting multiple permissions in one round. Please note that you have to call the Apply method at the end to actually perform the operation.
// set permissions on folder1, folder2 and file1 for two users and a group
SecurityHandler.CreateAclEditor()
.Allow(folder1.Id, user1.Id, false, PermissionType.Open, PermissionType.Custom01)
.Allow(folder2.Id, user2.Id, false, PermissionType.Open)
.Allow(file1.Id, editorsGroup.Id, false, PermissionType.Save)
.Apply();
As a side note: in most cases it is better to work with groups than users when assigning permissions. So it is advisable to give permissions to a group and put users into the group as members instead of assigning permissions to users directly.
Also: it is easier to maintain a simpler security structure, for example if you assign a permission on the parent container (e.g. a folder) instead of on individual files. Of course if you have to set permission per file, then it is fine.
OData api
The same api is available from the client through the REST api. Please take a look at the SetPermissions action in this article or the similar api in the JavaScript client library of sensenet.

Is there any way to attach the current data (as a .csv file) in PloneFormGen to a mailer?

We use PloneFormGen 1.7.12 using Plone 4.3.3. I have a request to include the current data in the email that the form is sending. We normally give editors access to the data to download, but the people he wants to send these to are not editors and I don't want to give them editor's permissions.
If it can't be added to the mailer, I guess I could create a role and give it just enough permissions for an authenticated user to download data. Would it work to copy the authenticated permissions over to a new role and add the PloneFormGen: Download Saved Input permission as well? I really don't like creating extra roles. In addition we would need to set up accounts for these people.
AFAIK not without coding :-)
Create a new DataSaveAdapter content type
Best way ist to inherit from the existing one and add a new field:
from Products.PloneFormGen.content.saveDataAdapter import FormSaveDataAdapter
SendDataAdapterSchema = FormSaveDataAdapter.schema.copy() + atapi.Schema((
atapi.StringField(
name='csv_recipients',
required=False,
widget=atapi.LinesWidget(
label=_(u'label_csv_recipients', default=u'CSV recipients'),
)
)
))
class SendDataAdapter(FormSaveDataAdapter):
implements(IPloneFormGenActionAdapter)
...
schema = SendDataAdapterSchema
...
The SaveDataAdapter provides a onSuccess method, where you can hook in and send your email
class SendDataAdapter(FormSaveDataAdapter):
...
def onSuccess(self, fields, REQUEST=None, loopstop=False):
""" saves input data and initiates mail"""
super(SendDataAdapter, self).onSuccess(fields, REQUEST, loopstop)
self.send_csv() # This is where you may implement sending the email.
Of course it needs some work to get it done (registering content type, etc.), but this should point you in the right direction.
Not really sure about your requirements, but in case you want to send the single-record in CSV-format as a mail when a form is submitted, you can customize the template of the mailer-adapter, like this:
<tal:block repeat="field options/wrappedFields | nothing">
"<span tal:replace="structure python:field.htmlValue(request)" />",
</tal:block>
Note, that this only works, if the mail's format is HTML, in plain text tal:repeat comes in the way, adding linebreaks between the values.
If you want to give a group of users permissions to view and download a save-adapter, go to PFG's controlpanel (append /prefs_pfg_permits to the site's URL), where it says "Download Saved Input" check the box for "Reader", then assign "Can edit"-permissioins via the sharing-tab of your save-adapter to the users and groups you want to be privileged.

Node "interest" notification in drupal

My question is quite simple (i think) but cannot find the right module for that.
I'm working on a small classified website in which i have a bunch of nodes. I display them using views.
I'd like for any (authenticated with role) user of the website to be able to click on a kind of button like "I'm interested" which will trigger an event doing various actions like 'changing one cck field' on that content-type and also send an e-mail to the author of the classified.
Pretty straight forward but no clue on where to start, which module should i use ?
For this, you can use the Flag Module and Rules Module
In your view, you can create a relationship to flags to allow the use of other 'fields'.
Using Rules, create a new rule that is triggered when a node is flagged (or unflagged). Rules allows you to do both: changing a CCK field and sending out emails when an event occurs.
Walk-through:
Install and enable the Flag Module and Rules Module
Create a new flag at "admin/build/flags"
Edit/Create you classified view, add a relationship to Flags: Node Flag and choose > the name of the flag you just created.
Under "fields" add Flags: Flag link and configure as you like
Add a new rule at "admin/rules/trigger/add" for the event A node has been flagged,
under "FLAG NAME"
Add action to perform of Populate a field under the heading CCK
Add action to perform of Send a mail to a user under the heading System
and configure your desired settings.
Then when a user clicks the "interested" flag a field will be populated and email sent.

Resources