Custom Workflow for content silos
Wondering if anyone has done anything like this:
1) I'd like to able to restrict a workflow to a section within the website:
Let's say i have 3 sections within my site, each with child sections. I have 3 groups of 6 groups of users: a group of content AUTHORS and content APPROVERS for each section. Each section has specific permissions on it to only allow editing by authors and approvers of that section.
I'd like this separation to also carry over to the workflow. Currently, when an author from any section sends an page for approval, the approvers from every section are notified.
2) I'd like to customize the NotifyGroup message to something more useful then "You have new items waiting approval."
I've been able integrate "PagesApprovalWorkflow.xamlx" into my project, modify it by following this post:
http://www.sitefinity.com/documentation/documentationarticles/developers-guide/sitefinity-essentials/modules/workflow-for-content-modules/custom-workflow as a test.
but I'm wondering if anyone has accomplished the goals i have described above?
thanks.
Hello,
1) Basically to be able to do this you should use customized workflows and most probably replace our GuardActivity with your custom GuardActivity. Our guard workflow activity is generally blocking access to actions based on whether a user is in a role associated with the approves or publishers workflow groups.
You can make your own logic there and throw WorkflowSecurityException if the conditions are not met. The downside of this approach is that our workflow menu rendering will not recognize your activity, so each user will see all the action buttons (DecisionActivities) that are possible for the current workflow status.
Once they click them they will receive the warning message that they are not allowed to say Approve this page etc.
Let's take pages for example:
My opinion is that the easiest way to do this is to use a Role naming convention.
For example create some roles named “WorkFlow-Courts”, “Workflow-TakingAction”, “Workflow-CurrentBusiness”.
Assign the respective users to these roles.
In your custom guard activity first check what the current page parent group is. If you have nested hierarchy you have to traverse the ancestors of the page node to find the group if not directly get PageNode.Parent.
Once you have the name of the group try to get the user roles for the user and check if the user has a role for this page group.
Please find below a sample guard activity that can be used. Replace our GuardActivity in your customized PagesApprovalWorkflow.xamlx your custom activity.
If your are going to have one level of approval – you can use just the StandardOneLevel approval flowchart and remove the Switch and the other flowcharts.
Please be aware that if you assign a role to a user the user first has to sign out and sign in – in order to receive this rolepublic
class
MyGuardActivity : CodeActivity
protected
override
void
Execute(CodeActivityContext context)
var property = context.DataContext.GetProperties()[
"workflowItem"
];
if
(property !=
null
)
var securedItem = property.GetValue(context.DataContext);
var pageNode = securedItem
as
Pages.Model.PageNode;
string
groupName=
null
;
while
(pageNode.Parent !=
null
)
var parent = pageNode.Parent;
if
(parent.NodeType == Pages.Model.NodeType.Group)
// you can add a check if it is one of your top groups
groupName = parent.UrlName;
break
;
pageNode = pageNode.Parent;
if
(
string
.IsNullOrEmpty(groupName))
return
;
string
neededRole =
string
.Concat(
"Workflow-"
, groupName);
var roles = SecurityManager.GetCurrentUser().GetRoles();
bool
isInRole=
false
;
foreach
(var role
in
roles)
if
(role.Equals(neededRole, StringComparison.InvariantCultureIgnoreCase))
isInRole =
true
;
break
;
if
(!isInRole)
throw
new
WorkflowSecurityException(String.Format(
"User should be in role 0"
, neededRole));
Were you able to get this functionality working? If so, do you have any code samples that you could share? I'm having the same type of issue. I need to have authors and approvers for departments. Let's say HR, Sales and IT. For example, HR authors can create pages in the HR section that can only be approved by HR approvers.
Another question I have is whether I can avoid creating a custom workflow by using the sync tool. If I create hr.mydomain.com and only allow hr users to author/approve content there, sales.mydomain.com and allow sales users to author/approve content there to be pushed to a production site that includes all sections, is this a realistic approach to handling this issue?
We also have this exact same requirement. We will have various blogs on our site, each maintained by a different division within our company. For each blog, we may have different sets of approvers/publishers. Since workflows only apply to Blogs overall (you can't tell it to apply only to one particular blog, for example), we are stuck with adding all possible approvers to our workflow, and then using blog-level permissions to only allow the particular set of approvers to actually be able to approve/publish blog posts for their division. This works for the most part, but all of the approvers get the notification email, regardless of whether or not they actually have permission to approve that particular blog post.
In addition, we may have the need for additional levels of approval (beyond the 2 that come out of the box). There has even been talk within our company of allowing one approver to decide "hey, this needs to also be routed to groups A, B, and C, for further approval", and therefore essentially dynamically choosing which other approvers might also need to see the content before publishing it.
It seems as if customizing workflows for Sitefinity is quite difficult. Are there future plans to make workflows within Sitefinity more robust out-of-the-box for more enterprise-level content authoring/approving needs?
We had also thought of the possibility of setting up multiple Sitefinity instances, one for each division, and then bringing together the content via the SiteSync tool, or via RSS feeds. Although that seems to be a bit cumbersome, especially when it comes to the need to upgrade all of those individual instances and keep them in sync.
Any guidance for us enterprise-level customers looking for more advanced workflow scenarios would be very much appreciated!
Hi,
At the current moments there are no plans to extend the workflows, I suppose you mean having a workflow editing tool that will allow workflow customizations to be made directly from sitefintiy backend applying changes instantly. The development team is aware of the benefits from having such feature, but for the time beeing this is not available and yet no plans are made towards this functionality.
Editing sitefintiy workflow and substituting it with custom workflow utilizing activitiy classes developed for specific purposes requires approach one described below, following windows workflow like development fixing the moving parts in the workflow designers with custom activity classes and sitefinity professional editions (custom workflow is available for sitefinity proessional and enterprise editions).
Sitefintiy have its own activity classes to handle the workflow in sitefintiy context and work with its data types and I think after a while in dealing with the activities and the worklfow designer one can deal with ease in implementing customizations to workflows.
Concerning having more than 2 level approavl workflow, the 2 level appraoval workflow when the item is passed trough second level workflow returns the item passed trough workflow with status Published, if the second level approval worklow is modified to return the item as awaiting further approval it will pass trough workflow again The implications come from the fast this is not available out of the box as in Administration->Worklfow there is no option to have third level approver.
Regards,
Stanislav Velikov
the Telerik team
Thanks for the suggestions. I have spent some time really digging into how the workflows work out of the box, and I'm getting more comfortable with them. One thing we'd really like to do is set up the workflow so that different blogs have different groups of people set up as approvers/publishers. The approach I was thinking of going with was creating a couple new custom permissions (under Advanced Settings -> Blogs -> Permissions -> BlogPost -> Actions), which would then allow us to assign roles to these new permissions on each blog. Then, I was planning on modifying the GuardActivity to look for these new permissions at the blog/content level, instead of or in addition to checking the permissions at the workflow level. When digging into this, I discovered that there is already a ContentPermissions property of the GuardActivity. I tried adding my new permission to this property (in the format "BlogPost.ApproveBlogPost"), but it doesn't seem to be working correctly. When I used Reflector to look at the GuardActivity code, it looks like it calls off to a securedContent.IsGranted method, but it doesn't actually use the results of that method to stop the user from going forward with that operation. And, even if it did, it looks like the code that inspects the workflow to build up the UI of allowed operations wouldn't honor this ContentPermissions property either, as it looks specifically only at the WorkflowPermissions property (this is inside the FlowchartWorkflowInspector.CanPassGuard method).
I plan on inheriting from the GuardActivity class, and in my version, actually check the ContentPermissions and stop the user from doing something they don't have permission to do, but I can't see a way to create my own implementation of a CanPassGuard method to also check this (there isn't an interface or anything that would allow me to plug in an alternative implementation). Is there a way this can be done? Or could Telerik implement this as a fix in a future version?
Finally, I'm also planning on inheriting from the NotifyGroup activity so that we only send the "You have content to approve" emails to the people who actually have the permission to do so (based on my new BlogPost.ApproveBlogPost permission). I believe that should be pretty straight forward.
Does this approach sound reasonable?
Oh, one more item: I noticed that sf_workflow_scope table has a column named content_filter_expression, which sounds like it might be able to be used to filter the workflow scope down to, say, a particular blog or via some other criteria. Is that actually implemented, and if so, what sort of syntax could we use in there?
Thanks!
Andy
I must say this conversion is really interesting... We have been working with Workflows for the first time and I'd love to know more about what can be done with the customization of the Sitefinity workflows. Did you get any response or did you have any success with this Andy?
We haven't heard much on this back from Telerik. However, we've had some success going our own way with a few customizations to the workflow logic. This is what we wanted to accomplish:
1. Each blog or list should have its own set of people who act as authors, approvers, and publishers (we are using the 2-step workflow).
2. Only the approvers or publishers configured for a given blog/list should get a notification email if some content is requiring their review.
3. If someone outside the configured set of authors, approvers, or publishers for a given blog or list attempts to author, approve, or publish content in that blog/list, they should be prevented from doing so.
Unfortunately, the built-in workflow functionality in Sitefinity only gives you a broad way to configure a set of approvers and publishers for blogs or lists overall... with no way to have separate sets of people for each individual blog or list. (It does allow you to have different sets of authors, however, through the use of permissions on each blog or list).
So, we realized that if authors could be set independently on each blog/list by using permissions, then perhaps we could create some custom permissions to do the same for approvers and publishers. So this is what we did:
1. We created custom "ApproveListItem", "PublishListItem", "ApproveBlogPost", and "PublishBlogPost" permissions. This can be done via the advanced settings UI, or simply by adding some stuff to the ListsConfig.config and BlogsConfig.config files. Here's what's inside our BlogsConfig.config file, for example:
<permissions>
<permission name="BlogPost">
<actions>
<add title="Approve Blog Post As Editor (Workflow Step 1)" type="None" name="ApproveBlogPost" />
<add title="Publish Blog Post (Workflow Step 2)" type="None" name="PublishBlogPost" />
</actions>
</permission>
</permissions>
2. Once these custom permissions are created, it's an easy matter of using the backend UI to set up what groups and/or users have each permission, per blog or list. We also needed to ensure that the users/groups who had the new Approve and Publish permissions also had the "Update this blog and manage its blog posts" permission.
3. Of course, we now needed a way to get the system to actually use these permissions somehow... this is where things got a little tricky. What we ended up doing is creating some custom Activities to plug into the Windows Workflow Foundation (xamlx) files. We created two classes:
a. CustomEmailNotifyGroup, which inherits Telerik.Sitefinity.Workflows.Activities.NotifyGroup, and overrides the GetEmails function so that it only gets emails for people who have the Approve or Publish permission. I put the code for this class at the bottom of this post.
b. CustomWorkflowGuard, which inherits System.Activities.CodeActivity. This activity is responsible for ensuring that the user has permission to do whatever the activity is that they are trying to do (approve or publish). We tried inheriting from Telerik.Sitefinity.Workflow.Activities.GuardActivity, thinking that we could just change the logic inside the existing acitivity that was already being used by the workflows. However, we ran into problems because unfortunately, that type is specifically being checked for inside Telerik.Sitefinity.Workflow.FlowchartWorkflowInspector.GetVisualDecisions, via this line of code: if (activity.GetType() == typeof(GuardActivity)) (this is a bad, bad practice, in my opinion, in regards to customizability of the software). So, instead, we decided to create a separate activity to do the additional checking that we required. We inserted this activity immediately after the DecisionActivity for the various paths through the workflow. You can find the code for this class at the bottom of this post.
4. We enabled the 2-level workflow for the Blog and List content types. Within the configuration of this workflow, we added the entire set of all users/groups that could act as Approvers or Publishers.
Now, the one issue we still have with this approach is that, since we weren't able to inherit from the existing GuardActivity, the backend will give the user options to do things that they might not actually have permission to do. However, once they try to do it, they'll get caught by our CustomWorkflowGuard and be prevented from actually accomplishing it. Not ideal, but good enough for us.
We really do hope that a future version of Sitefinity would introduce some more robust Workflow customization options. It's the one major area of Sitefinity that we feel is lacking, especially for trying to use it in an enterprise environment.
Andy
Example Code:
Imports
Telerik.Sitefinity.Workflow.Activities
Imports
System.Activities
Imports
Telerik.Sitefinity.Fluent.AnyContent.Implementation
Imports
Telerik.Sitefinity.Workflow.Model
Imports
Telerik.Sitefinity.Security.Model
Imports
Telerik.Sitefinity.Security
Imports
Telerik.Sitefinity.Abstractions
Imports
Telerik.Sitefinity.Security.Data
Imports
Telerik.Sitefinity.SitefinityExceptions
Imports
Telerik.Sitefinity.Blogs.Model
Imports
System.ComponentModel
Public
Class
CustomEmailNotifyGroup
Inherits
NotifyGroup
''' <summary>
''' Custom method that creates the notification message (e-mail) that is sent to approvers/publishers. This allows for custom e-mails to be sent.
''' </summary>
''' <param name="context"></param>
''' <remarks></remarks>
Protected
Overrides
Sub
Execute(context
As
System.Activities.CodeActivityContext)
'Get the item requiring approval
Dim
dataContext
As
WorkflowDataContext = context.DataContext
Dim
masterFluent
As
AnyDraftFacade =
DirectCast
(dataContext.GetProperties()(
"masterFluent"
).GetValue(dataContext), AnyDraftFacade)
Dim
item
As
Telerik.Sitefinity.GenericContent.Model.Content = masterFluent.
Get
()
'Reset the e-mail text
MyBase
.EmailText = getEmailText(item)
'This sends the message
MyBase
.Execute(context)
End
Sub
''' <summary>
''' Gets the emails that are defined on the permissions for each content type of blog post, list item, event, image, and document. Returns empty list if item being approved is of any other type.
''' </summary>
''' <param name="context"></param>
''' <returns></returns>
''' <remarks></remarks>
Public
Overrides
Function
GetEmails(context
As
System.Activities.CodeActivityContext)
As
List(Of
String
)
Dim
dataContext
As
WorkflowDataContext = context.DataContext
Dim
hashSet
As
System.Collections.Generic.HashSet(Of
String
) =
New
System.Collections.Generic.HashSet(Of
String
)()
Dim
workflowDefinition
As
WorkflowDefinition =
CType
(dataContext.GetProperties()(
"workflowDefinition"
).GetValue(dataContext), WorkflowDefinition)
Dim
groupName
As
String
=
Me
.Group
Dim
userManager
As
UserManager = userManager.GetManager
'get the item going through approval
Dim
masterFluent
As
AnyDraftFacade =
DirectCast
(dataContext.GetProperties()(
"masterFluent"
).GetValue(dataContext), AnyDraftFacade)
Dim
item
As
Telerik.Sitefinity.GenericContent.Model.Content = masterFluent.
Get
()
Dim
securedContent
As
ISecuredObject = TryCast(item, ISecuredObject)
'check if item is a valid type -- if it isn't, return no emails as it is probably something like creating a new blog, which should be a developer action
If
(
Not
(
TypeOf
item
Is
BlogPost
Or
_
TypeOf
item
Is
Telerik.Sitefinity.Lists.Model.ListItem
Or
_
TypeOf
item
Is
Telerik.Sitefinity.Events.Model.
Event
Or
_
TypeOf
item
Is
Telerik.Sitefinity.Libraries.Model.Image
Or
_
TypeOf
item
Is
Telerik.Sitefinity.Libraries.Model.Document))
Then
Return
New
List(Of
String
)
End
If
'get all permissions on the item
Dim
perms
As
IQueryable(Of Permission) = securedContent.GetActivePermissions()
'look up all principals on the item - think of these as the actual roles/users that have the valid permissions
Dim
principals
As
List(Of Guid) =
Me
.getPrincipalsFromPermissionsForGroup(groupName, perms)
'loop through each role
For
Each
principle
As
Guid
In
principals
'try catch in case returned principal type is for a user as opposed to a role -- not likely to happen
Dim
providers
As
ProvidersCollection(Of RoleDataProvider) = RoleManager.GetManager().Providers
For
Each
provider
As
RoleDataProvider
In
providers
Dim
manager
As
RoleManager = RoleManager.GetManager(provider.Name)
Try
'loop through users in that role
Dim
usersInRole
As
System.Collections.Generic.IList(Of User) = manager.GetUsersInRole(principle)
For
Each
user
As
User
In
usersInRole
If
user IsNot
Nothing
AndAlso
Not
String
.IsNullOrEmpty(user.Email)
AndAlso
Not
hashSet.Contains(user.Email)
Then
hashSet.Add(user.Email)
End
If
Next
Catch
e
As
ItemNotFoundException
'try to find the user that is being referenced by the principle in case the principle references a direct user
Dim
user
As
User = userManager.GetUser(principle)
If
user IsNot
Nothing
Then
hashSet.Add(user.Email)
End
If
End
Try
Next
Next
'return all the correct emails
Return
hashSet.ToList()
End
Function
''' <summary>
''' Creates a string that is used as text in an email
''' </summary>
''' <param name="item"></param>
''' <returns></returns>
''' <remarks></remarks>
Private
Function
getEmailText(
ByVal
item
As
Telerik.Sitefinity.GenericContent.Model.Content)
As
String
' Implementation removed from this sample...
Return
"Some Text"
End
Function
Private
Function
getPrincipalsFromPermissionsForGroup(
ByVal
group
As
String
,
ByVal
perms
As
IQueryable(Of Permission))
As
List(Of Guid)
'Return perms.Where(Function(p) p.SetName = "BlogPost" AndAlso p.IsGranted(New String() "ApproveBlogPostAsEditor")).Select(Function(p) p.PrincipalId).ToList()
'TODO bad hard coding, move to config
'if looking at the approve group, pull those roles. otherwise pull the publish roles
If
(group.Equals(
"Approve"
))
Then
Return
perms.Where(
Function
(p) (p.SetName =
"BlogPost"
AndAlso
p.IsGranted(
New
String
()
"ApproveBlogPost"
)) _
OrElse
(p.SetName =
"ListItem"
AndAlso
p.IsGranted(
New
String
()
"ApproveListItem"
)) _
OrElse
(p.SetName =
"General"
AndAlso
p.IsGranted(
New
String
()
"ApproveEvent"
)) _
OrElse
(p.SetName =
"Image"
AndAlso
p.IsGranted(
New
String
()
"ApproveImage"
)) _
OrElse
(p.SetName =
"Document"
AndAlso
p.IsGranted(
New
String
()
"ApproveDocument"
)) _
).
Select
(
Function
(p) p.PrincipalId).ToList()
ElseIf
(group.Equals(
"Publish"
))
Then
Return
perms.Where(
Function
(p) (p.SetName =
"BlogPost"
AndAlso
p.IsGranted(
New
String
()
"PublishBlogPost"
)) _
OrElse
(p.SetName =
"ListItem"
AndAlso
p.IsGranted(
New
String
()
"PublishListItem"
)) _
OrElse
(p.SetName =
"General"
AndAlso
p.IsGranted(
New
String
()
"PublishEvent"
)) _
OrElse
(p.SetName =
"Image"
AndAlso
p.IsGranted(
New
String
()
"PublishImage"
)) _
OrElse
(p.SetName =
"Document"
AndAlso
p.IsGranted(
New
String
()
"PublishDocument"
)) _
).
Select
(
Function
(p) p.PrincipalId).ToList()
Else
Throw
New
ArgumentException(
"Invalid group name passed to custom email notify group in workflow"
)
End
If
End
Function
End
Class
Imports
System.ComponentModel
Imports
Telerik.Sitefinity.Security.Model
Imports
Telerik.Sitefinity.Workflow.Exceptions
Imports
Telerik.Sitefinity.Security
Public
Class
CustomWorkflowGuard
Inherits
System.Activities.CodeActivity
Public
Overridable
Property
ContentPermissions
As
String
Protected
Overrides
Sub
Execute(context
As
System.Activities.CodeActivityContext)
'Now, ensure that the user has the appropriate content-level permission to approve/publish
If
Not
String
.IsNullOrEmpty(
Me
.ContentPermissions)
Then
Dim
descriptor
As
PropertyDescriptor = context.DataContext.GetProperties.Item(
"workflowItem"
)
If
(
Not
descriptor
Is
Nothing
)
Then
Dim
securedContent
As
ISecuredObject = TryCast(descriptor.GetValue(context.DataContext), ISecuredObject)
If
(
Not
securedContent
Is
Nothing
)
Then
If
Not
Me
.CheckCustomContentPermissions(securedContent)
Then
Throw
New
WorkflowSecurityException(
"User doesn't have one of the following permissions and is denied access: "
+
Me
.ContentPermissions)
End
If
End
If
End
If
End
If
End
Sub
Protected
Overridable
Function
CheckCustomContentPermissions(securedContent
As
ISecuredObject)
As
Boolean
Dim
permissionsToCheck =
Me
.ContentPermissions.Split(
New
Char
()
","
c)
For
Each
perm
In
permissionsToCheck
Dim
permSplit = perm.Split(
New
Char
()
"."
c)
Dim
permissionSet = permSplit(0)
Dim
actionName = permSplit(1)
If
securedContent.SupportedPermissionSets.Contains(permissionSet)
Then
If
securedContent.IsGranted(permissionSet,
New
String
() actionName)
Then
Return
True
End
If
End
If
Next
Return
False
End
Function
End
Class
Public
Class
MustHaveApproveContentPermission
Inherits
CustomWorkflowGuard
Public
Overrides
Property
ContentPermissions
As
String
Get
Return
"BlogPost.ApproveBlogPost,ListItem.ApproveListItem,General.ApproveEvent,Image.ApproveImage,Document.ApproveDocument"
End
Get
Set
(value
As
String
)
MyBase
.ContentPermissions = value
End
Set
End
Property
End
Class
Public
Class
MustHavePublishContentPermission
Inherits
CustomWorkflowGuard
Public
Overrides
Property
ContentPermissions
As
String
Get
Return
"BlogPost.PublishBlogPost,ListItem.PublishListItem,General.PublishEvent,Image.PublishImage,Document.PublishDocument"
End
Get
Set
(value
As
String
)
MyBase
.ContentPermissions = value
End
Set
End
Property
End
Class
Hello Andrew,
Thank you very much for taking the time to explain and include the code that you used for this. I have a similar challenge, and the information you provided is really helpful.
Regards,
Gary