W Nintex Workflow dla SharePoint (w każdej wersji), gdy używana jest akcja „Flexi task” i generuje ona, a następnie przydziela zadania dla konkretnych osób, gdy zalogowany użytkownik próbuje przejść do zatwierdzenia zadania (a nie jest to ta sama osoba, która jest do zadania przydzielona), aplikacja wyświetla komunikat: „You are not authorized to respond to this task.” – by móc wybrane zadanie zakończyć, użytkownik musi następnie kliknąć na link „delegate” i delegować zadanie na siebie. Dopiero wówczas może je zatwierdzić lub odrzucić.

Nintex Workflow task locked if not assigned

Jednym z kluczowych wymagań pozwalających użytkownikom SharePoint partycypować w przepływach pracy, zatwierdzać zadania, jest posiadanie uprawnień na poziomie „Współtworzenia” co najmniej na liście zadań (Workflow Tasks) i na liście historii przepływu pracy (Workflow History, źródło), a przynajmniej na zadaniu, które ma zatwierdzić lub odrzucić :).

W zasadzie oznacza to, że dowolny użytkownik, posiadający uprawnienia współtworzenia, ma możliwość zatwierdzenia bądź odrzucenia dowolnego, istniejącego zadania. Co więcej, w historii przepływu pracy na próżno można szukać śladów, że inny użytkownik zatwierdził zadanie nieprzydzielone do niego. jedynym miejscem jest historia wersji wybranego zadania. Zrozumiałe jest zatem przygotowanie takiego rodzaju zabezpieczenia przez Nintex.

Co w kwestii Nintex dla Office 365?

Uprawnienia, jakie musi posiadać użytkownik w Office 365 i on premise, by brać udział w przepływach pracy są takie same. Niestety, mechanizm zabezpieczający opisany powyżej w Nintex dla Office 365 nie istnieje (być może kiedyś powstanie, ale póki co się na to nie zanosi: UserVoice). Co za szkoda! Co za niedopatrzenie! Nie wszystko jednak stracone. Istnieje kilka alternatywnych rozwiązań dla tej potrzeby:

Przepływ pracy na liście Workflow Tasks

W mojej ocenie jest to najszybsze i najbardziej bezpieczne podejście zapewniające, że wyłącznie osoba przydzielona do zadania, będzie mogła je wykonać. To, co należy zrobić, ogranicza się do stworzenia przepływu pracy na liście „Workflow Tasks”, uruchamianego na zdarzenie utworzenia elementu. Następnie, korzystając z akcji „Office 365 Update Item Permissions” należy złamać dziedziczenie uprawnień na elemencie, wyczyści istniejące uprawnienia i dodać uprawnienia, na poziomie współtworzenia, dla użytkownika wskazanego w polu „Assigned To” (pamiętaj jednak o ograniczeniach w kontekście liczby unikalnych uprawnień).

   Pamiętaj, by umieścić tę akcję w „Action Set” z włączoną opcją podniesienia uprawnień, ponieważ użytkownik wykonujący workflow, może nie być tym samym, którego uprawnienia zostaną nadane i tym samym workflow się może zawiesić.

Break inheritance on the task, using Nintex Workflow

Można także dodać grupy administratorów lub kontrolerów, by zapewnić, iż nawet w przypadku nieobecności osoby przypisanej do zadania, ktoś (ale już uprawniony), będzie mógł się go podjąć i je zakończyć.

Ustawianie uprawnień zadania z poziomu przepływu głównego

Jednak co w przypadku, gdy zaistnieje potrzeba ustawiania różnych uprawnień w zależności od czynników w przepływie pracy elementu, który ma być zatwierdzony?

   Aktualizacja, 13.10.2017: Niestety nie jest możliwe użycie nowej funkcji, jaka została dodana do Nintex Workflow for Office 365 w październiku tego roku, która pozwala na uzyskanie kolekcji identyfikatorów wszystkich zadań wygenerowanych przez akcję „Start a task process” (źródło), ponieważ kolekcja budowana jest dopiero po zakończeniu się akcji (czyli zatwierdzeniu/ odrzuceniu zadań).

By to osiągnąć, należy użyć akcji „Query List”, która z listy Workflow Tasks pobierze wszystkie elementy, korzystając z filtra: Instance Id is equal to {Workflow Context:Instance Id} (dodaję także warunek, by pobierane były elementy nieukończone, żeby nie modyfikować już zakończonych zadań):

Query Nintex Workflow Tasks list action configuration

Mając jednak na uwadze, że sam workflow domyślnie zatrzymuje się, dopóki nie zostaną spełnione warunki zakończenia (Completion Criteria), toteż by móc jednocześnie przeprowadzać proces zatwierdzania i ustawiać zadaniom uprawnienia, konieczne jest użycie akcji „Parallel Branch”.

W drugiej gałęzi akcji:

  1. wykonywać obroty pętli tak długo, aż warunek przerwania nie jest spełniony (nie zakończył się proces zadania)
  2. w każdym obrocie pobierać listę identyfikatorów zadań
  3. w każdym obrocie przechodzić kolekcję identyfikatorów zadań wygenerowanych w ramach procesu
  4. następnie dla każdego zadania, ustawić uprawnienia
    Pamiętaj, by ustawianie upranień wykonywać w ramach akcji „Action Set” mającej podniesione uprawnienia, by zapewnić poprawność wykonania akcji.
  5. Po każdym obiegu zatrzymać się na 1 minutę, dzięki czemu pętla nie wykończy tenanta 🙂

Setting tasks permissions using Nintex

Pomimo tego, że to podejście również spełnia swoje zadanie, ma też swoje słabości: wygenerowane zadania mogą istnieć przez kilka minut zanim ich uprawnienia zostaną ustawione, akcja „Pause workflow” często zwalnia po czasie dużo większym, niż ustawiony, jednak z drugiej strony to podejście pozwala na realizację bardziej zaawansowanych scenariuszy, opartych np. o informacje związane z zatwierdzanym elementem, czy dane zapisane na formularzu elementu.

Napisanie dedykowanego skryptu w JavaScript

Można także napisać dedykowany kod w JavaScript i wstrzyknąć go do strony „EditForm” korzystając z web parta „Edytor skryptów” (w moim przypadku jest to plik HTML umieszczony za pomocą web parta „Edytor zawartości”). Kod może po prostu sprawdzać, czy zalogowany użytkownik to ten sam, który jest przydzielony do zadania i w przypadku gdy nie, obsłużyć takie zdarzenie. Kod poniżej właśnie tak działa – w sytuacji, gdy przydzielony użytkownik jest inny niż przeglądający, przykrywa formularz overlayem i umożliwia wyłącznie kliknięcie na „Delegate task” – co z kolei pokaże wyłącznie pole do zmiany osoby przypisanej do wykonania zadania. Co więcej, skrypt usuwa ze struktury DOM kod HTML związany z przyciskami zatwierdzania/ odrzucania, przez co nawet usunięcie overlaya korzystając z WebDevelopera niewiele pomoże:

<style type="text/css">

    #overlay {
        position: absolute;
        display: block;
        width: 100%; 
        height: 100%; 
        top: 0; 
        left: 0; 
        right: 0; 
        bottom: 0; 
        background-color: rgba(255, 255, 255, 0.5); 
        z-index: 1000; 
        cursor: not-allowed;
    }

    #overlay > p {
        position: absolute; 
        top: 25%; 
        transform: translateY(-50%); 
        font-size: 24px; 
        text-align: center; 
        background-color: #fff; 
        width: 100%
    }   

</style>

<script type="text/javascript" src="https://code.jquery.com/jquery-2.2.4.min.js"></script>
<script type="text/javascript">
    jQuery.noConflict();
    var currentUserId = _spPageContextInfo.userId;
    var assignedToId;

    Sys.WebForms.PageRequestManager.getInstance().add_pageLoaded(CheckIfAssignedTo);

    // hide form controls as fast, as they appear
    jQuery(function () {
        jQuery("div.ms-webpart-zone.ms-fullWidth").find("input[type='button']").each(function () { if (jQuery(this).val() !== "Cancel") jQuery(this).attr("style", "display:none"); });
        jQuery("a[id='Ribbon.ListForm.Edit.Commit.Publish-Large']").attr("style", "display:none");
        jQuery("li[id='Ribbon.ListForm.Edit.Clipboard']").attr("style", "display:none");
        jQuery("li[id='Ribbon.ListForm.Edit.Actions']").attr("style", "display:none");
    });

    function CheckIfAssignedTo() {

        // read current users' ID, try - for the displayForm, catch - for the editForm.
        try {
            assignedToId = ((jQuery("#SPFieldUserMulti").find("a[class='ms-peopleux-userdisplink ms-subtleLink']").attr("href")).match(/ID=(\d*)/))[1];
            processDisplayForm();
        }
        catch (e) {
            assignedToId = (JSON.parse(jQuery("input[id^='AssignedTo_']").val()))[0].EntityData.SPUserID;
            processEditForm();
        }

        console.log(assignedToId + " !== " + currentUserId);
    }

    function processEditForm() {
        // if current user is not the AssignedTo - remove controls from the form and display overlay.
        if (currentUserId != assignedToId) {
            jQuery.when(
                jQuery(jQuery("div.ms-webpart-zone.ms-fullWidth").attr("style", "position:relative;")).append("<div id=\"overlay\">" +
                    "<p>You are not allowed to response to this task.<br><a href=\"#reassign\">Please reassign it!</a></p>")
            )
                .done(function () {
                    jQuery("li[id='Ribbon.ListForm.Edit.Clipboard']").remove();
                    jQuery("li[id='Ribbon.ListForm.Edit.Actions']").remove();

                    jQuery('a[href="#reassign"]').click(function () {
                        jQuery("table[class='ms-formtable'] > tbody > tr").each(function () {

                            if ((jQuery(this).closest('tr').children('td:first').text()).trim() !== "Assigned To") jQuery(this).remove();
                            jQuery("a[id='Ribbon.ListForm.Edit.Commit.Publish-Large']").attr("style", "display:inline-block");
                            jQuery("div.ms-webpart-zone.ms-fullWidth").find("input[type='button']").each(function () { if (jQuery(this).val() === "Save") jQuery(this).attr("style", "display:block"); });
                            jQuery("#overlay").hide();

                        });
                    });

                });

        }
        // if current user is the AssignedTo - bring back all the form functionality
        else {
            jQuery("div.ms-webpart-zone.ms-fullWidth").find("input[type='button']").each(function () { if (jQuery(this).val() !== "Cancel") jQuery(this).attr("style", "display:block"); });
            jQuery("a[id='Ribbon.ListForm.Edit.Commit.Publish-Large']").attr("style", "display:inline-block");
            jQuery("li[id='Ribbon.ListForm.Edit.Clipboard']").attr("style", "display:inline-block");
            jQuery("li[id='Ribbon.ListForm.Edit.Actions']").attr("style", "display:inline-blocka");
        }
    }

    function processDisplayForm() {


    }

</script>

Można także stworzyć różne scenariusze działania, w zależności od tego, czy wyświetlany jest formularz edycji, czy podglądu – cokolwiek trzeba. Powyższe rozwiązanie to dopiero początek, jednak pozwalaj na zrealizowanie podstawowych wymagań:

Securing SharePoint task with JavaScript

Stworzenie widoku listy używając filtrów

Ostatnia, niespecjalnie bezpieczna metoda, jednak dająca użytkownikom dostęp do ich zadań, to przygotowanie domyślnego widoku listy, ustawiając filtr na „AssignedTo is equal to [Me]”. Gdy użytkownik wejdzie na listę, zobaczy listę wyłącznie zadań przypisanych do siebie. Komunikacja e-mail również informuje wyłącznie o zadaniach przypisanych do niego, także o ile nie mamy do czynienia z młodym hakerem,  próbującym dowieść słabości Twojego systemu, użytkownik prawdopodobnie nigdy nie domyśli się, że może przeglądać i zatwierdzać zadania nieprzypisane do niego.

Podsumowanie

Moim zdaniem, bez względu na to, jakie rozwiązanie zostanie wybrane krytyczne jest otrzymanie akceptacji interesariuszy i użytkowników aplikacji, oraz upewnienie się, że jest ono zgodne z politykami bezpieczeństwa firmy. Należy sprawdzić, czy proces jest krytyczny i czy faktycznie wymaga, by zatwierdzanie wykonywali wyłącznie przypisani użytkownicy – możliwe, że jest to mało istotny proces i dostarczenie wymaganych funkcjonalności nie wymaga skomplikowanych rozwiązań.

A może Ty masz inne, zweryfikowane przez siebie i używane w Twoich rozwiązaniach podejście, jeszcze inne od wymienionych powyżej? Daj znać! Zostaw info w komentarzu poniżej 🙂