Урок 17. Аутентификация пользователей в программе

Этим уроком мы открываем блок "Многопользовательский режим", в котором рассмотрим механизм аутентификации пользователей, статические и динамические права доступа, пользовательские настройки языка интерфейса и временные зоны.

Если хотите начать практику с этого урока, то вам необходимо развернуть учебный проект по инструкции в статье Разворачивание проекта.

При разворачивании проекта используйте backup базы данных, который можете найти в архиве из раздела Ответы прошлого урока. Скопируйте папки Forms, Workflow и Patterns в папку с развернутым проектом, например, в папку D:\WT\Projects\Template\Projects\1. Template.

Инструкция по подключению шаблонов находится по ссылке.

До этого урока в приложении мы работали под пользователем WS_GUEST, логин и пароль которого указаны в конфиге клиентской части (WorkflowForms.dll.config). В программе отсутствовала явная аутентификация, и клиентская часть подписывала все запросы к серверу токеном, полученным для гостевой учетной записи. В небольшом проекте без разделения прав доступа такой подход является рабочим.

Но в программе может возникнуть потребность в многопользовательском режиме и необходимость разделения прав доступа для пользователей. Тогда перед разработчиком встает задача реализовать механизм аутентификации в программе, настроить группы пользователей и выдать группам необходимые права доступа.

На этом уроке мы рассмотрим, как происходит аутентификация пользователя в платформе, и реализуем ее в программе, а также рассмотрим настройку прав доступа.

Аутентификация - процедура проверки подлинности, например проверка подлинности пользователя путем сравнения введенного им пароля с паролем, сохраненным в базе данных.

Авторизация - предоставление определенному лицу или группе лиц прав на выполнение определенных действий.

Список пользователей

Прежде чем рассказывать про аутентификацию и авторизацию, давайте создадим форму списка пользователей и карточку для редактирования.

База данных

В базе данных в таблице public.user хранится общий список пользователей системы:

Описание полей таблицы:

  • user_id - идентификатор глобального пользователя;

  • user_name - логин глобального пользователя;

  • user_full_name - полное имя глобального пользователя;

  • person - признак, определяющий, является ли данный глобальный пользователь реальным пользователем;

  • enabled - признак, определяющий, является ли данный глобальный пользователь включенным;

  • language_id - идентификатор языка глобального пользователя;

  • time_zone_info_id - идентификатор временной зоны глобального пользователя;

  • user_password - хеш пароля глобального пользователя.

В таблице есть записи системных пользователей:

  • Служба Workflow Engine ($workflow_engine$) - системный пользователь службы Workflow Engine, от имени которого совершаются некоторые автоматические действия с данными в базе данных;

  • WS. Гость (WS_GUEST) - системный пользователь, под которым запускается клиентское приложение.

Пользователь Администратор - реальный пользователь, под которым можно работать в программе.

Таблица template.user содержит идентификаторы пользователей, которые имеют доступ к бизнес-процессу Template.

Так же в эту таблицу можно добавить поля для индивидуальных пользовательских настроек, например, для доступа к какому-нибудь отчету.

Здесь стоит сказать о том, что WT-приложение может объединять несколько программ для разных бизнес-процессов, например, для автомойки и шиномонтажа.

Каждая программа будет иметь свой набор xml-файлов форм и свой серверный xml-файл с запросами. И, в большинстве случаев, серверная часть у них будет одна - нет смысла специально разделать серверные части, если в этом нет потребности. А значит, и базу данных можно использовать одну, но для каждого бизнес-процесса делать отдельную схему, чтобы разделить таблицы и функции.

Несмотря на то, что автомойка и шиномонтаж разные программы, какие-то пользователи могут иметь доступ к обеим программам, например, администраторы. Таким образом, в таблице public.user будут храниться логин и пароль, а так же пользовательские настройки, общие для автомойки и шиномонтажа. А в таблицах carwash.user (автомойка) и tireservice.user (шиномонтажа) будут храниться настройки пользователей, характерные для каждой программы. Например, доступ к какому-нибудь отчету.

Форма списка

Форма списка пользователей должна иметь вид:

Создайте форму для списка пользователей, используя паттерн ArchiveList:

На главной форме (TemplateStart.xml) в меню добавьте пункт Администрирование -> Пользователи..., по которому будет открываться новая форма.

Скорректируем текст запроса на список пользователей:

Template.xml
<SqlQuery Name="UserSelectSqlQuery">
  <Text>  
    SELECT
      user_id AS "UserId",
      user_full_name AS "Title",
      G.title AS "GroupTitle",
      UI.archive AS "Archive"
    FROM
      template.user_info UI
      JOIN template.user_group UG USING(user_id)
      JOIN template.group G USING (group_id)
    WHERE person
    ORDER BY user_full_name, user_id;
  </Text>
</SqlQuery>

В запросе используется template.user_info - это представление, которое динамически строится на основе двух таблиц: template.user и public.user.

В PostgreSQL представление (VIEW) - это виртуальная таблица, созданная запросом joins, соединяющим одну или несколько таблиц.

Про представления в PostgreSQL можно почитать в официальном документации по ссылке.

Обратите внимание на условие WHERE: фильтрация идет по полю person - такой проверкой мы отсекаем системных пользователей.

Скорректируем текст запроса для работы с архивными пользователями:

Template.xml
<SqlQuery Name="UserArchiveUpdateSqlQuery">
  <Text>
    UPDATE public.user
    SET
      enabled = NOT {Archive}
    FROM
      template.user U
    WHERE
      "user".user_id = U.public_user_id AND
      U.user_id = {EditedUserId};
  </Text>
</SqlQuery>

Переменная {UserId} является системной, и в ней хранится идентификатор пользователя, от которого пришел запрос с клиентской части. Разработчик может переопределить эту переменную, передав с формы одноименный параметр. Но в данном случае для идентификатора редактируемого пользователя будем использовать переменную {EditedUserId}, чтобы различать идентификаторы и иметь возможность использовать в одном запросе обе переменные.

template.user_try_delete(smallint)
CREATE OR REPLACE FUNCTION template.user_try_delete(in_user_id smallint)
  RETURNS boolean AS
$BODY$
DECLARE
  _public_user_id smallint;
BEGIN
  IF (SELECT NOT EXISTS(SELECT * FROM template.user WHERE user_id = in_user_id)) THEN
    RETURN TRUE;
  END IF;

  _public_user_id = (SELECT public_user_id FROM template."user" WHERE user_id = in_user_id);

  IF (SELECT used from template.is_used('user', 'user_id', ARRAY['user', 'user_group', 'user_form_info']::text[], ARRAY[]::text[], in_user_id)) THEN
    UPDATE public."user"
    SET enabled = False
    WHERE user_id = _public_user_id AND enabled;

    RETURN FALSE;
  ELSE
    DELETE FROM template."user"
    WHERE user_id = in_user_id;

    DELETE FROM public."user"
    WHERE user_id = _public_user_id;

    RETURN TRUE;
  END IF;
END;
$BODY$
  LANGUAGE plpgsql;

Отлично, теперь перейдем к карточке пользователя.

Карточка пользователя

Скорректируем запрос на получение данных редактируемого пользователя:

Template.xml
<SqlQuery Name="UserByIdSelectSqlQuery">
  <Text>
    SELECT
      user_name AS "UserName",
      user_full_name AS "UserFullName",
      group_id AS "GroupId",
      user_id = {UserId} AS "IsCurrent"
    FROM
      template.user_info
      JOIN template.user_group USING(user_id)
    WHERE
      user_id = {EditedUserId};
  </Text>
</SqlQuery>

В этом запросе мы как раз используем обе переменные {UserId} и {EditedUserId}. И с помощью системной переменной {UserId} определяем, является ли редактируемый пользователь текущим.

Так как информация о пользователях хранится в двух таблицах template.user и public.user, то и данные о новом пользователе мы должны добавлять в обе:

Template.xml
<SqlQuery Name="UserInsertSqlQuery">
  <Text>
    INSERT INTO public.user(user_name, user_full_name, user_password)
    VALUES({Name}, {FullName}, {Password});

    INSERT INTO template.user(public_user_id)
    VALUES(CURRVAL('public.user_id_seq'))
    RETURNING user_id;

    INSERT INTO template.user_group(user_id, group_id)
    VALUES(CURRVAL('template.user_id_seq'), {GroupId});
  </Text>
</SqlQuery>

В таблице template.user_group хранится информация о текущей группе каждого пользователя.

Template.xml
<SqlQuery Name="UserUpdateSqlQuery">
  <Text>
    UPDATE public.user
    SET
      user_name = {Name},
      user_full_name = {FullName}
    FROM
      template.user U
    WHERE
      "user".user_id = U.public_user_id AND
      U.user_id = {EditedUserId};

    UPDATE template.user_group
    SET
      group_id = {GroupId}
    WHERE
      user_id = {EditedUserId};
  </Text>
</SqlQuery>

Перейдем в файл карточки пользователя (TemplateUserEdit.xml) и создадим необходимые поля:

Форма будет иметь разный вид при создании и редактировании пользователя. Это необходимо для того, чтобы в целях безопасности не передавать на форму пароль, а точнее его хеш, и не усложнять форму и запрос для обработки пустого поля "Пароль" при редактировании пользователя. Смену пароля существующего пользователя вынесем на отдельную форму. Эту форму будем использовать на форме списка пользователей, чтобы администратор мог сбрасывать пароли, и на главной форме, чтобы пользователь мог сам сменить свой пароль. О разделении прав доступа поговорим позже в этом уроке.

Динамически менять высоту и ширину формы можно через параметры формы Height и Width соответственно. Добавим в тэг <Parameters> параметр Height с конструкцией <Switch> для вычисления высоты формы:

TemplateUserEdit.xml
<Parameter Name="Height">
  <Switch>
    <Case>
      <When>
        <Parameter Name="Edit" />
      </When>
      <Then>242</Then>
    </Case>
    <Case>290</Case>
  </Switch>
</Parameter>

Так как имя параметра формы совпадает с именем свойства формы, то форма автоматически подтянет его значение и изменит свой размер. Подобную особенность параметров формы мы уже использовали ранее, когда создавали параметр Title для заголовка формы (HeadLabel) и заголовка окна.

Список групп пользователей

Для объекта GroupComboBox (Группа пользователя) создадим загружающее соединение с данными:

TemplateUserEdit.xml
<DataConnection Name="GroupPrimaryGetDataConnection" Type="PrimaryGetDataConnection" Assembly="DataConnections">
  <SqlQuery Name="GroupSelectSqlQuery" Type="Select">
    <Workflow Name="Template" />
    <Fields>
      <Field Name="GroupId" />
      <Field Name="Title" />
      <Field Name="Archive" />
    </Fields>
    <Filter Type="Nested">
      <Or>
        <Filter>
          <Field NativeName="Archive" />
          <Value>False</Value>
          <DataType Type="BooleanDataType" />
        </Filter>
        <Filter>
          <Field NativeName="GroupId" />
          <Value>
            <DataConnection SourceDataConnection="UserPrimaryGetDataConnection">
              <Fields>
                <Field Name="GroupId" />
              </Fields>
            </DataConnection>
          </Value>
          <DataType Type="IntegerDataType" />
        </Filter>
      </Or>
    </Filter>
  </SqlQuery>
</DataConnection>

Запрос на получение списка групп пользователей:

Template.xml
<SqlQuery Name="GroupSelectSqlQuery">
  <Text>
    SELECT
      group_id AS "GroupId",
      title AS "Title",
      description AS "Description",
      archive AS "Archive",
      name IS NOT NULL AS "System"
    FROM
      template.group
    WHERE
      name IS DISTINCT FROM 'GuestGroup'
    ORDER BY title, group_id;
  </Text>
</SqlQuery>

В запросе из списка мы исключаем группу "Гости" (GuestGroup) - эта группы будет иметь права доступа только на получение списка пользователей для авторизации в программе.

Всего в программе есть три системных группы пользователей:

Пароль

Для ввода пароля в текстовое поле PasswordTextBox используйте тэг <Password> со значением True. В таком случае при вводе текста пользователь будет видеть только символы звездочек ( * ).

В базе данных в таблице public.user в поле user_password хранится не сам пароль, а его хеш, который необходимо вычислять с помощью команды ComputeHashCommand с использованием выбранного алгоритма хеширования. По умолчанию используется алгоритм SHA512.

Создадим такую команду:

TemplateUserEdit.xml
<Command Name="PasswordComputeHashCommand" Type="ComputeHashCommand" Assembly="Commands">
  <Source>
    <Object Name="PasswordTextBox" />
  </Source>
</Command>

Будем вызывать ее в SaveSequentialCommand перед вызовом команды UserInsertSaveCommand, а результат выполнения будем передавать в параметр Password сохраняющего соединения с данными с именем UserInsertSetDataConnection.

Уникальность логина

При добавлении или редактировании пользователя необходимо делать проверку на уникальность введенного логина.

Создадим запрос для проверки:

Template.xml
<SqlQuery Name="UserExistsSelectSqlQuery">
  <Text>
    SELECT EXISTS (
      SELECT user_name
      FROM
        public.user PU
        JOIN template.user U ON U.public_user_id = PU.user_id
      WHERE
        user_name = {UserName} AND
        (U.user_id != {EditedUserId} OR {EditedUserId} IS NULL)
    ) AS "UserExists";
  </Text>
</SqlQuery>

Создадим загружающее соединение, чтобы проверять существование пользователя с введенным логином:

TemplateUserEdit.xml
<DataConnection Name="UserExistsPrimaryGetDataConnection" Type="PrimaryGetDataConnection" Assembly="DataConnections">
  <ManualLoad>True</ManualLoad>
  <SqlQuery Name="UserExistsSelectSqlQuery" Type="Select">
    <Workflow Name="Template" />
    <Fields>
      <Field Name="UserExists" />
    </Fields>
    <Parameters>
      <Parameter NativeName="UserName" RefreshQuery="False">
        <Value>
          <Object Name="NameTextBox" />
        </Value>
      </Parameter>
      <Parameter NativeName="EditedUserId" RefreshQuery="False">
        <Value>
          <Parameter Name="EditedUserId" />
        </Value>
      </Parameter>
    </Parameters>
  </SqlQuery>
</DataConnection>

И создадим условие для проверки результата выполнения запроса:

TemplateUserEdit.xml
<Condition Name="UserNotExistsCondition" Type="NotEqualCondition" Assembly="Conditions">
  <Items>
    <Item>
      <DataConnection SourceDataConnection="UserExistsPrimaryGetDataConnection">
        <Fields>
          <Field Name="UserExists" />
        </Fields>
      </DataConnection>
    </Item>
    <Item>True</Item>
  </Items>
</Condition>

Создадим команду на обновление UserExistsPrimaryGetDataConnection:

TemplateUserEdit.xml
<Command Name="UserExistsDataConnectionRefreshCommand" Type="DataConnectionRefreshCommand" Assembly="Commands">
  <DataConnections>
    <DataConnection Name="UserExistsPrimaryGetDataConnection" />
  </DataConnections>
</Command>

Также понадобится команда для вывода сообщения пользователю:

TemplateUserEdit.xml
<Command Name="UserExistsMessageBoxCommand" Type="MessageBoxCommand" Assembly="Commands">
  <Caption>Сохранение</Caption>
  <Text>Пользователь с таким именем уже существует.</Text>
  <Icon Type="Warning" />
  <Buttons Type="Ok" />
</Command>

Таким образом, команда SaveSequentialCommand будет иметь вид:

TemplateUserEdit.xml
<Command Name="SaveSequentialCommand" Type="SequentialCommand" Assembly="Commands">
  <Commands Lock="True">
    <Command Name="UserExistsDataConnectionRefreshCommand" />
    <If>
      <When>
        <Condition Name="UserNotExistsCondition" />
      </When>
      <Then>
        <If>
          <When>
            <Parameter Name="Edit" />
          </When>
          <Then>
            <Command Name="UserUpdateSaveCommand" />
          </Then>
          <Else>
            <Command Name="PasswordComputeHashCommand" />
            <Command Name="UserInsertSaveCommand" />
            <Command Name="EditedUserIdValueSetCommand" />
          </Else>
        </If>
        <Command Name="UpdatedTrueValueSetCommand" />
        <Command Name="FormCloseCommand" />
      </Then>
      <Else>
        <Command Name="UserExistsMessageBoxCommand" />
      </Else>
    </If>
  </Commands>
</Command>

Запустите проект и проверьте работу формы, создав несколько пользователей.

Изменение логина текущего пользователя

При изменении логина текущего пользователя (поле IsCurrent в запросе UserByIdSelectSqlQuery) необходимо чтобы пользователь самостоятельно повторно зашел в программу с новым логином.

Чтобы предупреждать пользователя об изменении логина текущей учетки, добавьте кнопку "Редактировать логин" в карточку пользователя:

При нажатии на кнопку текстовое поле "Логин" должно становиться доступный для изменения, и пользователю должно отображаться предупреждение вида:

<Command Name="NameInformationMessageBoxCommand" Type="MessageBoxCommand" Assembly="Commands">
  <Caption>Изменение логина пользователя</Caption>
  <Text>После изменения логина текущего пользователя потребуется повторная аутентификация.</Text>
  <Icon Type="Information" />
  <Buttons Type="Ok" />
</Command>

В качестве самостоятельной работы можете решить "задачку со звездочкой".

Пока мы просто предупреждаем пользователя, о необходимости повторно войти в программу при изменении логина у текущего пользователя программы. Но при возвращении на форму списка пользователей упадет ошибка вида:

Запрос "UserSelectSqlQuery" в процессе "Template" не может быть выполнен, т.к. пользователь "administrator" не найден или отключен.

Реализуйте самостоятельно логику, чтобы в подобном случае пользователь сразу возвращался на стартовую форму, где открывалась бы форма входа в программу.

Используйте материал из раздела Аутентификация - практика, там будем создавать команду ReloginFormShowCommand для повторного входа в программу.

Аутентификация - теория

Первым делом необходимо получить список активных пользователей. Для этого создается отдельный SQL-запрос, который доступен для группы GuestGroup.

По умолчанию все запросы к серверу подписываются гостевой учеткой WS_GUEST (находится в группе пользователей GuestGroup), логин и пароль которой указаны в файле WorkflowForms.dll.config.

Полученный список пользователей отображается в выпадающем списке на форме входа:

После того, как пользователь выбрал учетную запись и ввел пароль, клиентская часть хеширует пароль безопасным алгоритмом SHA-512 и отправляет на сервер запрос с данными в зашифрованном виде.

Когда запрос приходит на сервер, он попадает сначала в веб-сервер Kestrel, на котором запущена веб-служба, а затем перенаправляется в серверное приложение.

Kestrel представляет кроссплатформенный веб-сервер и по умолчанию включается в проект ASP.NET Core.

На сервере механизм аутентификации и авторизации реализован с помощью JWT-токенов. Когда Workflow Engine получает запрос на аутентификацию пользователя, полученные логин и хеш пароля сверяются с теми, которые хранятся в таблице public.user в базе данных. Если логин и хеш пароля совпали - генерируется JWT-токен, который возвращается клиентскому приложению вместе с временем жизни этого токена и одноразовым токеном для повторной генерации основного JWT-токена.

JWT (или JSON Web Token) представляет собой веб-стандарт, который определяет способ передачи данных о пользователе в формате JSON в зашифрованном виде.

Клиентское приложение хранит JWT-токен и подписывает им все последующие запросы к серверу. По истечении времени жизни JWT-токена клиентская часть отправляет запрос на обновление JWT-токена.

Аутентификация - практика

Создадим пустую форму для входа в программу (TemplateLogin.xml):

Перейдем в файл главной формы (TemplateStart.xml) и создадим две команды на открытие формы входа:

TemplateStart.xml
<Command Name="LoginFormShowCommand" Type="FormShowCommand" Assembly="Commands">
  <Xml Type="Path">TemplateLogin.xml</Xml>
  <Show Type="Modal" />
</Command>

<Command Name="ReloginFormShowCommand" Type="FormShowCommand" Assembly="Commands">
  <Xml Type="Path">TemplateLogin.xml</Xml>
  <Show Type="Modal" />
</Command>

Команду LoginFormShowCommand будем вызывать при старте приложения для первичного входа в приложение.

Для вызова команды ReloginFormShowCommand на стартовой форме добавим в главное меню пункт Файл -> Войти как..., чтобы у пользователей была возможность входа в программу под другой учеткой без необходимости закрывать приложение.

Также это позволит нам просматривать форму входа при ее редактировании.

Форма входа

Нам понадобятся иконки. Скачайте архив с изображениями и разархивируйте его в папку проекта \Template\Projects\1. Template\Forms\Images\24x24.

Создайте на форме выпадающий список пользователей (UserComboBox) и текстовое поле для ввода пароля (PasswordTextBox).

Список пользователей

Для получения списка активных пользователей создадим запрос:

Template.xml
<SqlQuery Name="UserLoginSelectSqlQuery">
  <Text>
    SELECT
      user_id AS "UserId",
      user_name AS "UserName",
      user_full_name AS "UserFullName"
    FROM
      template.user_info UI
      JOIN template.user_group UG USING(user_id)
      JOIN template.group G USING (group_id)
    WHERE
      UI.person AND NOT UI.archive AND
      G.name IS DISTINCT FROM 'GuestGroup';
  </Text>
</SqlQuery>

Создадим разрешение, которое будет предоставлять доступ к запросам, общим для всех групп пользователей:

Template.xml
<Permission Name="BaseViewSqlQueryPermission" Type="SqlQueryPermission">
  <SqlQueries>
    <SqlQuery Name="UserLoginSelectSqlQuery" />
  </SqlQueries>
</Permission>

Добавим роль для базовых прав доступа:

Template.xml
<Role Name="BaseRole">
  <Permissions>
    <Permission Name="BaseViewSqlQueryPermission" />
  </Permissions>
</Role>

Добавим эту роль в группу GuestGroup.

Вернемся на форму входа (TemplateLogin.xml) и создадим соединения с данными для получения списка активных пользователей и для фильтрации выбранного пользователя:

TemplateLogin.xml
<DataConnection Name="UserPrimaryGetDataConnection" Type="PrimaryGetDataConnection" Assembly="DataConnections">
  <SqlQuery Name="UserLoginSelectSqlQuery" Type="Select">
    <Workflow Name="Template" />
    <Fields>
      <Field Name="UserId" />
      <Field Name="UserName" />
      <Field Name="UserFullName" />
    </Fields>
  </SqlQuery>
</DataConnection>

<DataConnection Name="UserSecondaryGetDataConnection" Type="SecondaryGetDataConnection" Assembly="DataConnections">
  <SourceDataConnection Name="UserPrimaryGetDataConnection" />
  <Filter>
    <Field NativeName="UserId" />
    <Value>
      <Object Name="UserComboBox" />
    </Value>
  </Filter>
</DataConnection>

Аутентификация

Для аутентификации в программе используется команда типа LoginCommand. Создадим эту команду:

TemplateLogin.xml
<Command Name="LoginCommand" Type="LoginCommand" Assembly="Commands">
  <Condition Name="MandatoryFieldsAllowedNestedCondition" />
  <UserName>
    <DataConnection SourceDataConnection="UserSecondaryGetDataConnection">
      <Fields>
        <Field Name="UserName" />
      </Fields>
    </DataConnection>
  </UserName>
  <Password>
    <Object Name="PasswordTextBox" />
  </Password>
</Command>

Команда сама вычислит хеш строки пароля и отправит на сервер логин пользователя и хеш пароля в зашифрованном виде.

Создадим условия проверки результата выполнения команды авторизации:

TemplateLogin.xml
<Condition Name="LoginCommandOkEqualCondition" Type="EqualCondition" Assembly="Conditions">
  <AlwaysChange Value="True" />
  <Items>
    <Item>
      <Command Name="LoginCommand" />
    </Item>
    <Item>Ok</Item>
  </Items>
</Condition>

<Condition Name="LoginCommandFailEqualCondition" Type="EqualCondition" Assembly="Conditions">
  <AlwaysChange Value="True" />
  <Items>
    <Item>
      <Command Name="LoginCommand" />
    </Item>
    <Item>Fail</Item>
  </Items>
</Condition>

Создадим команду вывода сообщения об ошибке аутентификации:

TemplateLogin.xml
<Command Name="LoginFailedMessageBoxCommand" Type="MessageBoxCommand" Assembly="Commands">
  <Caption>Ошибка аутентификации</Caption>
  <Text>Пароль указан неверно.\rПопробуйте еще раз.</Text>
  <Icon Type="Warning" />
  <Buttons Type="Ok" />
</Command>

Вызывать команду LoginCommand будем по кнопке LoginButton. Также можно выполнять вызов по нажатию клавиши Enter при вводе пароля в текстовом поле PasswordTextBox. Для проверки нажатия клавиши будем использовать условие KeyPressCondition:

TemplateLogin.xml
<Condition Name="PasswordTextBoxEnterKeyPressCondition" Type="KeyPressCondition" Assembly="Conditions">
  <Object Name="PasswordTextBox" />
  <Key Code="13" />
</Condition>

Создадим необходимые Execution для обработки нажатия клавиши Enter и для обработки результатов выполнения команды авторизации:

TemplateLogin.xml
<Execution>
  <ConditionExpression>
    <Condition Name="PasswordTextBoxEnterKeyPressCondition" />
  </ConditionExpression>
  <Commands>
    <Command Name="LoginCommand" />
  </Commands>
</Execution>

<Execution>
  <ConditionExpression>
    <Condition Name="LoginCommandOkEqualCondition" />
  </ConditionExpression>
  <Commands>
    <Command Name="UpdatedTrueValueSetCommand" />
    <Command Name="FormCloseCommand" />
  </Commands>
</Execution>

<Execution>
  <ConditionExpression>
    <Condition Name="LoginCommandFailEqualCondition" />
  </ConditionExpression>
  <Commands>
    <Command Name="LoginFailedMessageBoxCommand" />
  </Commands>
</Execution>

Отлично!

Отображение формы входа

Когда пользователь запускает приложение, первое что он увидит, должна быть форма входа в программу. Но делать форму TemplateLogin.xml стартовой - не самая лучшая идея.

Связано это с тем, что стартовая форма является главным окном программы. Когда закрывается главное окно, то закрываются все дочерние окна программы. Следовательно, мы не сможем закрывать форму авторизации после входа в программу.

Сворачивать стартовую форму авторизации тоже будет не самым лучшим решением, так как при закрытии главной формы по крестику пользователь надеется закрыть программу. Но вместо этого он попадет на форму авторизации.

Нам остается только делать хитрый финт: запускать главную форму (TemplateStart.xml) без обновления данных, делать ее невидимой и открывать форму входа. Далее на главной форме проверять значение параметра Updated дочерней формы. Если значение будет False, то закрывать главную форму и прекращать работу программы, а если будет True, то делать главную форму видимой и обновлять данные.

Для начала нужно перевести все PrimaryGetDataConnection в ручной режим загрузки (ManualLoad=True) и добавить команду AllPrimaryGetDataConnectionRefreshCommand для их обновления:

TemplateStart.xml
<Command Name="AllPrimaryGetDataConnectionRefreshCommand" Type="DataConnectionRefreshCommand" Assembly="Commands">
  <Parallel>True</Parallel>
  <DataConnections>
    <DataConnection Name="CityPrimaryGetDataConnection" />
    <DataConnection Name="ClientPrimaryGetDataConnection" />
  </DataConnections>
</Command>

Не добавляйте в команду OrderPrimaryGetDataConnection. Для его обновления уже есть команда OrderDataConnectionRefreshCommand - будем использовать ее, так как в будущих уроках пригодится разделение на разные команды.

Чтобы форма была невидимой (прозрачной), в тэг <Form> добавим атрибут Opacity со значением 0. А для изменения прозрачности формы создадим команду:

TemplateStart.xml
<Command Name="ShowFormCommand" Type="ValueSetCommand" Assembly="Commands">
  <Form>
    <Property Name="Opacity">1</Property>
  </Form>
</Command>

Добавим Execution для открытия формы входа и обработки результата:

TemplateStart.xml
<Execution>
  <ConditionExpression>
    <Condition Name="FormLoadedCondition" />
  </ConditionExpression>
  <Commands>
    <Command Name="LoginFormShowCommand" />
  </Commands>
</Execution>

<Execution>
  <ConditionExpression>
    <Command Name="LoginFormShowCommand" Parameter="Updated" />
  </ConditionExpression>
  <Commands>
    <Command Name="ShowFormCommand" />
    <Command Name="AllPrimaryGetDataConnectionRefreshCommand" />
    <Command Name="OrderDataConnectionRefreshCommand" />
  </Commands>
</Execution>

<Execution>
  <ConditionExpression>
    <Not>
      <Command Name="LoginFormShowCommand" Parameter="Updated" />
    </Not>
  </ConditionExpression>
  <Commands>
    <Command Name="FormCloseCommand" />
  </Commands>
</Execution>

У нас еще есть команда ReloginFormShowCommand для аутентификации в момент работы в программе. Давайте добавим Execution для обработки результата выполнения этой команды:

TemplateStart.xml
<Execution>
  <ConditionExpression>
    <Command Name="ReloginFormShowCommand" Parameter="Updated" />
  </ConditionExpression>
  <Commands>
    <Command Name="AllPrimaryGetDataConnectionRefreshCommand" />
    <Command Name="OrderDataConnectionRefreshCommand" />
  </Commands>
</Execution>

Отлично! Запустите приложение и проверьте отображение формы входа. Попробуйте войти в программе.

Права доступа

После успешной аутентификации при попытке обновить данные сервер выбросит ошибку вида:

Пока что мы работали в программе под пользователем WS_GUEST и все права доступа на запросы мы добавляли в GuestGroup. Но это гостевая учетка, и ее права доступа должны быть ограничены, если в программе поддерживается многопользовательский режим и разделение прав доступа.

Для реальных пользователей есть группы AdministratorGroup и UserGroup. Давайте добавим эти группы в серверный xml-файл. Затем скопируем в них роли из GuestGroup, в которой оставим только BaseRole, чтобы при запуске приложения на форме входа мы могли видеть список активных пользователей.

Подробнее о правах доступа поговорим в следующем уроке, где рассмотрим рекомендации по формированию Permission и разделению их на роли.

Запустите программу и проверьте аутентификации и загрузку данных.

Смена пароля

В программе должна быть возможность менять пароль пользователей через интерфейс.

На форме списка пользователей (TemplateUserList.xml) создайте кнопку для вызова формы смены пароля. Для кнопки используйте изображение password.png из ранее скачанного архива.

Самостоятельно создайте форму смены пароля (TemplateUserPasswordEdit.xml) вида:

На форму смены пароля необходимо передавать идентификатор пользователя, которому меняется пароль. Создайте для этого параметр EditedUserId. Используйте его в запросе UserByIdSelectSqlQuery для получения логина редактируемого пользователя и флага IsCurrent.

По кнопке сохранить необходимо вычислять хеш нового пароля и сохранять изменение в базу данных. Если меняется пароль для текущего пользователя (поле IsCurrent в запросе UserByIdSelectSqlQuery), то необходимо вызывать команду LoginCommand для повторной аутентификации с новым паролем.

Создадим запрос для смены пароля:

Template.xml
<SqlQuery Name="UserPasswordUpdateSqlQuery">
  <Text>
    UPDATE public.user
    SET user_password = {Password}
    FROM
      template.user U
    WHERE
      "user".user_id = U.public_user_id AND
      U.user_id = {EditedUserId};
  </Text>
</SqlQuery>

Создайте новое разрешение UserPasswordEditSqlQueryPermission, в которое добавьте запросы UserByIdSelectSqlQuery и UserPasswordUpdateSqlQuery. Разрешение добавьте в роль UserPasswordRole, которую укажите для каждой группы.

Текущий пользователь должен иметь возможность поменять свой пароль без необходимости заходить в список пользователей. Особенно, если у пользователя нет прав доступа к списку. Разделение прав доступа на форме рассмотрим в следующих уроках.

На главной форме (TemplateStart.xml) в меню добавьте пункт Файл -> Сменить пароль... для открытия формы изменения пароля для текущего пользователя.

Чтобы на форму смены пароля передавать идентификатор текущего пользователя (параметр EditedUserId) создайте на главной форме соединение с данными:

TemplateStart.xml
<DataConnection Name="UserCurrentPrimaryGetDataConnection" Type="PrimaryGetDataConnection" Assembly="DataConnections">
  <ManualLoad>True</ManualLoad>
  <SqlQuery Name="UserCurrentSelectSqlQuery" Type="Select">
    <Workflow Name="Template" />
    <Fields>
      <Field Name="UserId" />
    </Fields>
  </SqlQuery>
</DataConnection>

Обратите внимание, что мы указали тэг <ManualLoad> со значение True, так как нам не нужно обновлять данные о текущем пользователе до тех пор, пока пользователь не прошел аутентификацию в программе.

Добавим соединение с данными в команду AllPrimaryGetDataConnectionRefreshCommand, чтобы оно обновлялось при успешной аутентификации пользователя.

В серверный xml-файл скопируем текст запроса UserCurrentSelectSqlQuery:

Template.xml
<SqlQuery Name="UserCurrentSelectSqlQuery">
  <Text>
    SELECT
      user_id AS "UserId",
      user_name AS "UserName",
      user_full_name AS "UserFullName"
    FROM
      template.user_info
    WHERE
      user_id = {UserId};
  </Text>
</SqlQuery>

Добавим запрос в BaseViewSqlQueryPermission, так как он является общим для всех пользователей.

Запустите приложение и проверьте загрузку формы и смену пароля.

Итоги

На уроке мы рассмотрели процесс аутентификации пользователя, добавили возможность редактировать список пользователей, а также настроили статические права доступа для групп "Администраторы" и "Пользователи".

Ответы

В архиве присутствуют xml-файлы форм и серверный xml-файл, также лежит бэкап базы данных и файл с запросами на изменение структуры базы данных - с помощью файлов можете проверить себя.

Last updated