Сервисы сервера приложений#

Сервисы реализованы в опциональном модуле sys, который входит в дистрибутив Global Server Scala Edition. Так же модуль может быть подключен в конфигурационном файле сервера global3.config.xml:

<sbt name="sys"
      lazyLoad="false"
      source="C:\global3\sysappbin\"
      sourceMode="Jar"
      binaryFolder="C:\global3\sysappbin\"
/>

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

Более подробную информацию смотрите в руководстве по администрированию сервера.

SSH консоль сервера#

Введение#

Global 3 SE Server включает в себя SSH (Secure Shell) сервер, к которому возможно подключиться с помощью любого SSH-терминала. С помощью команд командной строки возможно:

  • Смотреть статистику, логи, и управлять клиентскими сессиями

  • Перезагружать код прикладных приложений

  • Перезапускать Web-приложения

  • Выполнять SQL и Jexl-скрипты

Конфигурирование#

Конфигурационный файл global3.config.xml может содержать секцию <ssh/>:

<ssh defaultDb="PostgreSql" port="22"/>
  • defaultDb - Алиас базы данных по умолчанию
    Данное значение используется в качестве значения параметра по умолчанию для ssh-команды login. По имени базы будет определены имя и пароль пользователя, для подключения к базе.

  • port – Порт ssh-сервера
    По умолчанию: 22.
    Данное значение может быть jvm опцией

    -Dglobal.ssh.port=xxxx  
    

    или параметрами командной строки при запуске Global3 сервера

    -global.ssh.port xxxx 
    

Учётные данные пользователей, имеющих права на ssh-сединения, указываются в секции <security/> конфигурационного файл global3.config.xml:

<security>
  <users>
    <user name="admin" password="admin" roles="ssh"/>
  </users>
</security>
  • user – Пользователь

  • password | encryptedPassword – Пароль
    Пароль может быть указан как в явном, так и в зашифрованном виде.

  • roles – Роли
    Список ролей, через запятую, доступных пользователю.

Шифрование пароля#

Для шифрования пароля необходимо запустить Global 3 Server c парамерами

Start.bat -encryptPassword password -masterPassword masterpassword

Где:

  • -encryptPassword
    Пароль который необходимо зашифровать

  • -masterPassword
    Ключ шифрования, если не указан используется секретный ключ по умолчанию.

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

Подключение#

Подключиться к SSH-серверу возможно любым SSH-терминалом. Рекомендуемым клиентом для подключения является PuTTY.

Документация по putty

Основные параметры#

Для подключения необходимо указать:

  • Host Name
    host или user@host

  • Port
    22 или пользовательский

  • Connection type
    SSH

Что бы не вводить значения полей при повторном запуске PuTTY, можно сохранить параметры подключения по умолчанию, нажав кнопку «Save».

Пример запуска из командной строки:

"C:\Program Files\PuTTY\putty.exe" -ssh localhost -P 22 -l admin -pw admin

Логирование#

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

  • Session Logging = All session output

  • Log file name = полный путь к файлу

Внимание

В адресах пути:

  • Символ \ необходимо экранировать удвоением
    Пример: \\

  • Символ / экранировать не надо

Подключение с использованием SSH-RSA ключа#

При подключении к SSH из *.bat иил *.sh можно воспользоваться RSA-ключами вместо пароля.

"C:\Program Files\PuTTY\putty.exe" -ssh localhost -P 22 -l admin -i ssh_admin_private.ppk

Необходимые файлы размещены в подкаталоге .\ssh.

Ключи для подключения к пользователю admin выдаются по запросу.

Запуск терминала#

При первом подключении к серверу будет выдано сообщение с запрашивающие разрешение на подключение к серверу, нажмите: Да.

Команды#

Для получения актуальной справки по доступным командам, выполните команду help.

Список команд:

  • alter server mode service|normal
    Переключает сервер в сервисный режим и обратно. В сервисном режиме возможно подключение пользовательских сессий только от имени системного пользователя сервера приложений, указанного в конфигурационном файле.

    <systemUsers>
            <user name="system" password="system"/>
    </systemUsers>
    
  • attach session {sid}
    Подключение к существующей пользовательской сессии.

  • attach db {dbAlias} [as sys]
    Подключение к базе данных для выполнения сервисных операций и/или Jexl-скриптов.

  • сlear
    Очищает экран терминала.

  • clear persistence cache [{dbAlias}]
    Очищает Shared-кэш объектов.

  • compare applib {path}
    Сравнивает jar-файлы в каталоге (или zip-архиве) {path} с jar-файлами в каталоге SBT.jarFolder, соответствующему базе данных, к которой выполнено подключение командой attach db.

    Выполняется сравнение версий модулей.
    Выполняется сравнение *.odm.xml файлов на предмет существования новых атрибутов

  • copy applib [force] {path}
    Выполняет копирование jar-файлов из каталога {path} в каталог SBT.jarFolder, соответствующий базе данных, к которой выполнено подключение командой attach db. При указании ключевого слова force перед копированием не выполняется сравнения *.odm.xml файлов. Сравнение версий модулей выполняется в любом случаи.

  • Init schema
    Выполняет создание/обновление объектов схемы БД

  • execute {expression}
    Выполняет однострочное Jexl-выражение

  • exit
    Закрывает SSH-подключение

  • jexl [{file}]
    Переключает терминал в режим ввода и выполнения

Выполнение SQL#

Для выполнения sql-скрипта:

  1. Подключитесь к сессии командами login, attach или set sid.

  2. Перейдите в режим ввода скрипта командой sql.

  3. Введите текст скрипта.

  4. Выполните скрипт
    Для этого введите символ / с новой строки.

  5. Выйдите из режима ввода скрипта
    Для этого введите символ / с новой строки.

Пример:

login admin/admin@postgres  
sql  
INSERT INTO gs3_roottest (
    id, 
    idClass
  ) VALUES (
  (select  
    nextval('GS3_ROOTTEST_SEQ'))
    , 12351
  ) ON CONFLICT DO NOTHING;  
/  
/

Выполнение SQL скрипта из файла#

Для выполнения sql-скрипта из файла:

  1. Подключитесь к сессии.

  2. Выполните команду sql {file}.
    Где {file} – путь к файлу к файлу на сервере.

    Внимание

    Файл должен находится на локальном диске сервера.

Пример:

login admin/admin@postgres
sql D:\\svn\\depot\\ASSource\\sysapplication\\ssh\\src\\test\\java\\ru\\bitec\\app\\ssh\\shh_sql_exams.txt

Содержимое файла:

INSERT INTO gs3_roottest (id, idClass) VALUES ((select nextval('GS3_ROOTTEST_SEQ')), 12351) ON CONFLICT DO NOTHING;
INSERT INTO gs3_roottest (id, idClass) VALUES ((select nextval('GS3_ROOTTEST_SEQ')), 12351) ON CONFLICT DO NOTHING;
/
INSERT INTO gs3_roottest (id, idClass) VALUES ((select nextval('GS3_ROOTTEST_SEQ')), 12351) ON CONFLICT DO NOTHING;
INSERT INTO gs3_roottest (id, idClass) VALUES ((select nextval('GS3_ROOTTEST_SEQ')), 12351) ON CONFLICT DO NOTHING;
/
/

Jexl скрипты#

Выполнение Jexl скрипта#

Для выполнения Jexl-скрипта необходимо:

  1. Подключитесь к сессии

  2. Переключитесь в режим ввода скрипта командой jexl.

  3. Введите текст скрипта.

  4. Выполните скрипт
    Для этого введите символ / с новой строки.

  5. Выйдите из режима ввода
    Для этого введите символ / с новой строки.

Пример 1:

login admin/admin@postgres
jexl
var name = Btk_ClassApi.getCanonicalClassName("Btk_Object");
Btk_ClassApi.getApiByCanonicalClassName(name);
/
/

Пример 2 (для dataInstall):

login admin/admin@postgres
jexl
Bbb_DBTypeApi.dataInstall();
Btk_Pkg.commit();
/
/

Пример 3:

login admin/admin@postgres
jexl
Btk_Pkg.setRWSharedUOWEditType();
Prs_EntTransApi.dataInstall();
Btk_Pkg.commit();
/
/

Выполнение Jexl скрипта из файла#

Для выполнения Jexl-скрипта из файла необходимо:

  1. Подключитесь к сессии

  2. Выполнить команду jexl {file}
    Где {file} – путь к файлу к файлу на сервере.

    Внимание

    Файл должен находится на локальном диске сервера.

Пример:

login
jexl D:\\svn\\depot\\ASSource\\sysapplication\\ssh\\src\\test\\java\\ru\\bitec\\app\\ssh\\shh_jexl_exams.txt

Содержимое файла:

var name = Btk_ClassApi.getCanonicalClassName("Btk_Object");
Btk_ClassApi.getApiByCanonicalClassName(name);
/
/

Контекст выполнения Jexl скрипта#

Возможны следующие контексты выполнения Jexl-скрипта.

Пользовательская сессия#
>login user/password@alias
>jexl
jexl>
/
/
>

В данном контексте доступны все Api-классы, присутствующие в SBT, соответсвующего пользовательской сессии.

База данных#
>attach db alias
>jexl
jexldb>
/
/
>

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

  • initschema()
    Выполняет инициализацию/обновление схемы БД в соответствии с текущей прикладной кодовой базой.

Системный контекст#
>attach db alias as sys
>jexl
jexlsys>
/
/
>

Данный контекст является административным и предназначен для управления сервером приложений. Доступны следующие методы:

  • upgrade({release_path})
    Выполняет инициализацию/обновление схемы БД в соответствии с текущей прикладной кодовой базой.
    Где:
    {release_path} – путь к каталогу или zip-архиву с релизом прикладных модулей

Выполнение командного файла ssh#

Для выполнения командного файла из командной строки можно выполнить:

"C:\Program Files\PuTTY\putty.exe" -ssh localhost -P 22 -l admin -pw admin -m shh_logger_test_script.txt 

Внимание

В данном примере файл shh_logger_test_script.txt размещён на диске клиентской машины, он считывается в момент подключения к SSH-серверу. Пути к файлам в скрипте, являются локальными для сервера.

Содержимое файла:

login
log-info app el

execute Btk_ClassApi.getCanonicalClassName("Btk_Object")

jexl
var name = Btk_ClassApi.getCanonicalClassName("Btk_Object");
Btk_ClassApi.getApiByCanonicalClassName(name);
/
/

jexl D:/svn/depot/ASSource/sysapplication/ssh/src/test/java/ru/bitec/app/ssh/shh_jexl_exams.txt
jexl D:\\svn\\depot\\ASSource\\sysapplication\\ssh\\src\\test\\java\\ru\\bitec\\app\\ssh\\shh_jexl_exams.txt

sql
INSERT INTO gs3_roottest (id, idClass) VALUES ((select nextval('GS3_ROOTTEST_SEQ')), 12351) ON CONFLICT DO NOTHING;
/
/

sql D:\\svn\\depot\\ASSource\\sysapplication\\ssh\\src\\test\\java\\ru\\bitec\\app\\ssh\\shh_sql_exams.txt

log-off all
logout

Логирование#

При выполнении скрипта, по умолчанию, в ssh-консоль выводится результат выполнения команды или сообщение об ошибке со стеком вызова.

Для вывода в ssh-консоль дополнительных логов, необходимо их включить. В консоль можно вывести логи следующих типов:

  • oper
    Логи из классов с namespace ru.bitec.engine.model.operation.

  • sql
    Логи sql-вызовов с уровня jdbc-соединения с базой.

  • script
    Логи из скриптового языка в режиме совместимости с Global 1. При работе со Scala не имеют смысла.

  • app
    Логи из классов прикладной логики с namespace ru.bitec.app.*.
    Пример отправки сообщения:

    Logger.Factory.get(Xxx_XxxApi.class).info("Текст сообщения")
    
  • el
    Логи из инфраструктуры EclipseLink. В основном, это sql-вызовы, в более компактном виде, чем логи jdbc.

  • all
    Все выше перечисленные типы логов.

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

  • log-off

  • log-error

  • log-warn

  • log-info

  • log-debug

  • log-trace

Команды указаны в порядке уменьшения уровня логов.

В результате выполнения этой команды:

log-info app el

в ssh-лог будут попадать сообщения типов app и el для уровней логирования: error, warn, info.

Для отключения вывода сообщений в ssh-лог необходимо вызвать команду:

log-off all
Перенаправление ssh-лога Putty в файл#

При выполнении скриптов из командной строки, часто необходимо перенаправить вывод в файл. Для ssh-консоли Putty, это выполняется передачей параметра -sessionlog {имя_файла.txt}

"C:\Program Files\PuTTY\putty.exe" -ssh localhost -P 22 -l admin -pw admin -m shh_logger_test_script.txt -sessionlog sessionlog.txt

FAQ#

Зависает ssh-терминал на этапе подключения к серверу.#

При запуске Putty из скрипта при первом подключении к серверу выдается сообщение PuTTY Security Alert, что может приводить к зависанию скрипта.

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

echo yes | %plink% -ssh localhost -P 2222 -l admin -i ssh_admin_private.ppk 

Примечание

Plink также входит в состав дистрибутива PuTTY.

WebSocket консоль сервера#

Введение#

Global 3 SE Server предоставляет консоль управления, доступную через WebSocket соединение.

С помощью команд передаваемых через WebSocket возможно:

  • Перезагружать код прикладных приложений

  • Выполнять Jexl-скрипты

Алгоритм работы с консолью#

  • Открытие WebSocket-соединения

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

  • Отправка исполняемых команд

  • Закрытие WebSocket-соединения

Открытие WebSocket соединения#

Для окрытия WebSocket соединения с консолью сервера, необходимо выпонить http-запрос по адресу: ws[s]://{server[:port]}/app/sys/ws/console.

Формат команды#

Командой является строка в формате:

{command}[\n{arguments}]

где:

{command} - строка команды. Может состоять из одного или нескольких слов.
\n - символ новой строки #10. Является разделителем команды и её аргументов. Не обязателен, если у команды нет аргуметтов.
{arguments} - строка аргументов команды. Может содержать любые символы, включая перносы строк.

Формат результата выполнения команды#

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

{
  "success":true, 
  "data": null, 
  "exception":null, 
  "exceptionStack":null
}

где:

success - Флаг успешности выполнения команды: true|false
data - Результат выполненения команды: null|"string"|JSON
exception - Сообщение возникшего исключения: null|"string"
exceptionStack - Стек возникшего исключения: null|"string"

Список команд#

  • login\n
    {user}/{password}@{database}

    Выполняет аутентификацию пользователя user в базе данных database и запускает рабочий сеанс пользователя.

    Использование команды login, в качестве способа уатентификации, было выбрано по причине невозможности использования http-заголовков некоторыми WebSocket-клиентами. Например: JMeter WebSocket Load Testing Sampler.

  • logout

    Закрывает рабочий сеанс пользователя.

  • reload sbt

    Выполняет перезагрузку прикладного кода текущего решения.

  • reload sbt force

    Выполняет перезагрузку инфраструктуры EclipseLink, прикладного кода и общих библиотек текущего решения.

  • jexl\n
    // Произвольный текст Jexl-скрипта
    return 1 + 2;

    Любое значение, возвращённое из Jexl-скрипта, будет преобразовано в строку.

Java пример взаимодествия с консолью#

Библиотеки, используемые в примере клиента:

  • «org.asynchttpclient» % «async-http-client» % «2.12.3»

  • «com.fasterxml.jackson.core» % «jackson-databind» % «2.8.9»

package ru.bitec.app.examples.ws.console;

import org.asynchttpclient.AsyncHttpClient;
import org.asynchttpclient.Dsl;
import org.asynchttpclient.ws.WebSocket;
import org.asynchttpclient.ws.WebSocketListener;
import org.asynchttpclient.ws.WebSocketUpgradeHandler;

import com.fasterxml.jackson.databind.ObjectMapper;

import javax.websocket.CloseReason;
import java.io.Serializable;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

public class WsConsoleClientTest {

  public static void main(String[] args) throws Exception {
      try (WsConsoleClient client = WsConsoleClient.open("ws://localhost:8080/app/sys/ws/console")) {
          client.login("admin", "admin", "pgtest");
          System.out.println("1 + 2 = " + client.evaluateJexl("return 1 + 2;"));
          client.reloadSbt(false);
          client.logout();
      }
  }   

  public class WsConsoleClient implements AutoCloseable {

    public static WsConsoleClient open(String url) throws Exception {
        return open(url, 3000);
    }

    public static WsConsoleClient open(String url, int timeout) throws Exception {
        return new WsConsoleClient().connect(url, timeout);
    }

    private final ObjectMapper objectMapper_ = new ObjectMapper();
    private int commandTimeout_ = 60000;
    private AsyncHttpClient asyncHttpClient_;
    private WebSocket webSocketClient_;
    private volatile CompletableFuture<String> webSocketResultFuture_;
    private final WebSocketUpgradeHandler wsHandler = new WebSocketUpgradeHandler.Builder().addWebSocketListener(new WebSocketListener() {
        @Override
        public void onOpen(WebSocket websocket) {
            // WebSocket connection opened
        }

        @Override
        public void onClose(WebSocket websocket, int code, String reason) {
            if (code != CloseReason.CloseCodes.NORMAL_CLOSURE.getCode()) {
                webSocketResultFuture_.completeExceptionally(new Exception(reason));
            }
        }

        @Override
        public void onError(Throwable t) {
            webSocketResultFuture_.completeExceptionally(t);
        }

        @Override
        public void onTextFrame(String payload, boolean finalFragment, int rsv) {
            webSocketResultFuture_.complete(payload);
        }
    }).build();

    public WsConsoleClient connect(String url, int timeout) throws Exception {
        AsyncHttpClient syncHttpClient = Dsl.asyncHttpClient();
        try {
            webSocketClient_ = syncHttpClient
                    .prepareGet(url)
                    .setRequestTimeout(timeout)
                    .execute(wsHandler)
                    .get();
        } catch (Exception e) {
            syncHttpClient.close();
            throw e;
        }
        asyncHttpClient_ = syncHttpClient;
        return this;
    }

    public void close() throws Exception {
        if (asyncHttpClient_ != null) {
            try {
                if (webSocketClient_ != null && webSocketClient_.isOpen()) {
                    try {
                        webSocketClient_.sendCloseFrame().get();
                    } finally {
                        webSocketClient_ = null;
                    }
                }
            } finally {
                asyncHttpClient_.close();
            }
        }
    }

    public int getCommandTimeout() {
        return commandTimeout_;
    }

    public void setCommandTimeout(int commandTimeout) {
        this.commandTimeout_ = commandTimeout;
    }

    public void login(String user, String password, String database) throws Exception {
        sendCommand("login", String.format("%s/%s@%s", user, password, database));
    }

    public void logout() throws Exception {
        sendCommand("logout");
    }

    public String evaluateJexl(String script) throws Exception {
        return sendCommand("jexl", script);
    }

    public void reloadSbt(boolean force) throws Exception {
        sendCommand("reload sbt" + (force ? " force" : ""));
    }

    String sendCommand(String command) throws Exception {
        return sendCommand(command, null);
    }

    String sendCommand(String command, String args) throws Exception {
        validateConnection();
        if (command == null || command.trim().isEmpty()) {
            throw new Exception("Command can not be null or empty.");
        }
        webSocketResultFuture_ = new CompletableFuture<>();
        try {
            String payload = command + "\n" + (args != null ? args : "");
            webSocketClient_.sendTextFrame(payload).get();
        } catch (Exception e) {
            webSocketResultFuture_.completeExceptionally(e);
        }
        return parseResponse(webSocketResultFuture_.get(commandTimeout_, TimeUnit.MILLISECONDS));
    }

    private String parseResponse(String response) throws Exception {
        ConsoleResponse consoleResponse = objectMapper_.readValue(response, ConsoleResponse.class);
        if (consoleResponse.success) {
            return consoleResponse.data;
        } else {
            throw new Exception(consoleResponse.exception);
        }
    }

    private void validateConnection() throws Exception {
        if (webSocketClient_ == null) {
            throw new Exception("WebSocket connection is not connected.");
        }
        if (!webSocketClient_.isOpen()) {
            throw new Exception("WebSocket connection is closed");
        }
        CompletableFuture<String> webSocketResultFuture = webSocketResultFuture_;
        if (webSocketResultFuture != null && !webSocketResultFuture.isDone()) {
            throw new Exception("Prior console command is not completed. Wait for the result of the previous command.");
        }
    }

    private final static class ConsoleResponse implements Serializable {

        private static final long serialVersionUID = -5577579081118070434L;
        /**
         * Флаг успешного выполнения запроса. false, если при обработке запроса возникло исключение.
         */
        private boolean success = false;
        /**
         * Строковые данные
         */
        private String data;
        /**
         * Текст возникшего исключения
         */
        private String exception;
        /**
         * Стек возникшего исключения
         */
        private String exceptionStack;

        public boolean getSuccess() {
            return success;
        }

        public void setSuccess(boolean success) {
            this.success = success;
        }

        public String getData() {
            return data;
        }

        public void setData(String data) {
            this.data = data;
        }

        public String getException() {
            return exception;
        }

        public void setException(String exception) {
            this.exception = exception;
        }

        public String getExceptionStack() {
            return exceptionStack;
        }

        public void setExceptionStack(String exceptionStack) {
            this.exceptionStack = exceptionStack;
        }
    }
  }
}

Jexl через SOAP XML#

Введение#

В Global 3 SE Server реализован SOAP XML сервис, позволяющий выполнять Jexl-скрипт в контексте пользовательской сессии.

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

Читайте в разделе Аутентификация в REST/SOAP-сервисах.

Схемы#

WSDL описание сервиса доступно по адресу:

http://{server:port}/app/sys/soap/sys-service-1.0.0?wsdl

XSD схемы POJO запроса и ответа доступны по адресу:

http://{server:port}/app/sys/soap/sys-service-1.0.0?xsd=1

Передача бинарных данных с использованием MTOM#

Сервис поддерживает оптимизированную передачу бинарных данных в сервис и обратно, через поля attachment объектов JexlSoapRequest и JexlSoapResponse.

Доступ к данным SOAP-запроса из прикладного кода#

В прикладном коде Api-/Pkg-классов, выполняющемся в результате вызова Jexl-скрипта из SOAP-сервиса, доступен объект SoapContext. SoapContext предоставляет доступ к данным SOAP-запроса и SOAP-ответа. Для получения контекста, выполните:

val soapContextOpt = SoapContext()

Методы доступные в SoapContext:

  • hasInputAttachment: Boolean
    Указывает на наличие у SOAP-запроса прикреплённых с использованием MTOM бинарных данных

  • getInputData: String
    Возвращает строковые данные SOAP-запроса.

  • forInputStream(foo: InputStream => Unit): Unit
    Предоставляет доступ к прикреплённым бинарным данным

  • setOutputData(data: String): Unit \ Устанавливает строковые данные в SOAP-ответ.

  • forOutputStream(foo: OutputStream => Unit): Unit
    Предоставляет доступ к выходному потоку бинарных данных передаваемых средствами MTOM

Методы обработки данных SOAP-запроса из Jexl-скрипта#

В пакете Gtk_SoapPkg хранятся методы, использующие SoapContext:

  • setOutputData(value: AnyRef): Unit
    Установливает вывод результата выполнения soap-запроса.

  • inputStreamToTemp: File
    Создает временный файл с данными из прикреплённых данных soap-запроса

  • attachFile(file: File): Unit
    Принимает файл, закачаивает его данные в выходной поток бинарных данных soap-запроса

    Пример ипользования из Jexl-скрипта:

    var f = new("java.io.File", 'C:\users\userName\data.txt');
    Gtk_SoapPkg.setOutputData('Данные в прикрепленном файле');
    Gtk_SoapPkg.attachFile(f);

Данный скрипт, отправленный через SOAP-запрос, в ответ запишет

  • в data - строку „Данные в прикрепленном файле“

  • в attachment - закодированный base64 файл

Jexl через REST API сервис c использованием прикладных пакетов#

Введение#

В Global 3 SE Server реализован REST API сервис, позволяющий выполнять Jexl-скрипт в контексте пользовательской сессии. В данной главе описывается выполенение jexl-скриптов c использованием прикладных пакетов, наследуемых от RestPkg.

Выполнение Jexl в контексте главной выборки приложения описано в отдельной главе.

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

Читайте в разделе Аутентификация в REST/SOAP-сервисах.

Адреса#

Доступно с AS 1.14 RC7:

  • http://{server:port}/app/sys/rest/es/pkg/Btk_JexlGatePkg/execute

  • http://{server:port}/app/sys/rest/ss/pkg/Btk_JexlGatePkg/execute

где:

  • {server:port}
    Адрес подключения к серверу

  • app/sys
    Системный прикладной модуль

  • rest
    Шлюз для rest запросов

  • es / ss
    Группировка по времени жизни gtk-сессии (Exclusive Session) / (Shared Session)

  • pkg
    Узел для доступа к пакетам.

  • Btk_JexlGatePkg
    Имя прикладного пакета, предназначенного для выполнения jexl-скриптов (подробнее о Rest-пакетах)

Администрирование выполнения jexl-скриптов#

Администрирование выполнения jexl-скриптов осуществляется через право роли на «Функции Jexl». Также можно отдельно ограничить доступ роли к Btk_JexlGatePkg.

Структура запроса#

Тело POST запроса имеет вид

{
    "jexl": "jexl-скрипт"
}

Формат результата выполнения команды#

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

{
    "exception":null, 
    "exceptionStack":null,
    "data": null, 
    "success":true, 
    "exceptionName":null, 
}

где:

exception - Сообщение возникшего исключения: null|"string"
exceptionStack - Стек возникшего исключения: null|"string"
success - Флаг успешности выполнения команды: true|false
data - Результат выполненения команды: null|"string"|JSON|Boolean
exceptionName - Имя класса исключения: null|"string"

Пример использования#

Для выполнения jexl-скрипта будем использовать пакет Btk_JexlGatePkg и Shared Session, тогда адрес запроса будет выглядеть так:

  • http://localhost:8080/app/sys/rest/ss/pkg/Btk_JexlGatePkg/execute

В качестве примера Jexl-скрипт будет выполнять проверку, является ли пользователь супер-пользователем:

{
    "jexl": "Btk_UserApi.isSuperUser()"
}

После выполнения POST-запроса сервер вернёт ответ:

{
    "exception": null,
    "exceptionStack": null,
    "data": false,
    "success": true,
    "exceptionName": null
}

Запрос выполнился без ошибок и jexl-скрипт вернёт значение false.

Ограничения#

Время выполнения jexl-скрипта ограничено временем жизни сессии. Размер jexl-скрипта не ограничен.

REST сервис c обработкой http-запроса в прикладном пакете#

Введение#

В Global 3 SE Server реализован REST-сервис, позволяющий выполнять обработку HTTP-запроса в прикладном пакете, в контексте прикладной сессии.

Возможны 2 режима обработки запросов:

  • Exclusive Session - с сохранением состояния между запросами. Rest- и Gtk-сессии создаются при первом обращении клинета к сервису и закрываются по таймауту или при явном указании на необходимость закрытия сессий по завершению запроса.

  • Shared Session - без сохранения состояния между запросами. Каждый запрос обрабатывается в новых Rest- и Gtk-сессиях. Доступно с AS 1.14 RC7.

Пакет должен быть унаследован от одного из трейтов RestPkg, RestESPkg, RestSSPkg.

class Xxx_XxxPkg extends Pkg with RestPkg {
}  
object Xxx_XxxPkg extends PkgFactory[Xxx_XxxPkg]{
}

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

Читайте в разделе Аутентификация в REST/SOAP-сервисах.

Методы RestPkg / RestESPkg / RestSSPkg#

  • get(relativePath: String): AnyRef
    Вызывается при поступлении GET запроса.
    Допускается возврат: null, None, String, ResponseBuilder.

  • post(relativePath: String): AnyRef
    Вызывается при поступлении POST запроса.
    Допускается возврат: null, None, String, ResponseBuilder.

  • onActivate()
    Вызывается при подключении новой gtk-сессии

  • onDeactivate()
    Вызывается при закрытии gtk-сессии

  • beforeReload(keyBundle: KeyBundle)
    Вызывается перед перезагрузкой SBT

  • afterReload(keyBundle: KeyBundle)
    Вызывается после перезагрузки SBT и пересоздании gtk-сессии

  • isSupportsSharedSession(): Boolean
    Метод указывает, что пакет может обрабатывать запросы в режиме разделяемой GTK-сессии.

  • isSupportsExclusiveSession(): Boolean
    Метод указывает, что пакет может обрабатывать запросы в режиме эксклюзивной GTK-сессии.

Допустимые результаты методов get() и post()#

От типа возвращённого значения будет зависеть способ формирования ответа на Http-запрос.

  • null,None
    Ответ будет сформирован через RestfulContext().get.responseBuilder

  • String
    В ответ будет записано возвращённая строка

  • ResponseBuilder
    Ответ будет сформирован через возвращенный ResponseBuilder. Он может быть равен RestfulContext().get.responseBuilder, либо сформирован произвольным образом.

  • Response
    Ответом будет возвращенное значение.

Адреса#

Доступно с AS 1.14 RC7:

  • http://{server:port}/app/sys/rest/es/pkg/{Xxx_XxxxPkg}/{relativePath}

Доступно с AS 1.14 RC7:

  • http://{server:port}/app/sys/rest/ss/pkg/{Xxx_XxxxPkg}/{relativePath}

где:

  • {Xxx_XxxxPkg}
    Имя прикладного пакета, унаследованного от RestfulPkg

  • {relativePath}
    Произвольный путь, который будет передан в методы get/post прикладного пакета

  • http://{server:port}
    Адрес подключения к серверу

  • sys
    Системный прикладной модуль, в будущем будет app/sys

  • rest
    Шлюз для rest запросов

  • es / ss
    Группировка по времени жизни gtk-сессии (Exclusive Session) / (Shared Session)

  • pkg
    Узел для доступа к пакетам.

Рабочее пространство#

Workspace - Рабочее пространство.

Используется для возможности параллельной работы нескольких gtk сессий в рамках одного пользователя в es режиме.

Для предотвращения неконтролируемого разрастания сессий, количество сессий на пользователя в эксклюзивном режиме ограничено. На одного пользователя и один workspace может существовать только одна сессия.

Рабочее пространство задается в http заголовке:

  • Workspace
    Имя рабочего пространства пользователя.

Exclusive Session#

Принцип работы сервиса#

  1. Получение HTTP-запроса (GET или POST)

  2. Проверка авторизационных данных пользователя.

  3. Получение, захват существующего рабочего сеанса или создание нового.

  4. Вызов метода прикладного пакета get(…) или post(…), соответственно
    В данном методе производится формирование тела http-ответа

  5. Отправка ответа

Жизненный цикл http-сессий#

При обращении к rest - сервису.

  1. Если cookie JSESSIONID не задан, создается новая http сессия

  2. Иначе, используется сессия с переданным идентификатором
    Если переданный id не корректный создается новая http сессия

  3. Обработка запроса

  4. Возврат результата
    При этом cookie JSESSIONID будет содержать идентификатор http сессии

Время жизни http-сессии 15 минут. При отсутствии обращений к http-сессии в течении этого интервала, сессия уничтожается, и установленные в её атрибуты значения становятся не доступны.

Жизненный цикл gtk-сессий#

Gtk-сессия существует в разрезе 4-х параметров:

  1. Алиас база данных
    Получается из http-заголовка Database. Если заголовок не передан, используется алиас из конфигурационного файла сервера.

  2. Имя пользователя
    Получается из авторизационных данных. Передаётся в http-заголовке Authorization

  3. Имя прикладного пакета
    Получается из строки адреса

  4. Рабочее пространство
    Получается из http-заголовка Workspace. Если не задано, используется Default

Одной http-сессии (одному JSESSIONID) может соответствовать только одна gtk-сессия. При обращении к другому пакету из одной http-сессии, предыдущая gtk-сессия будет деактивирована и закрыта.

Таймаут gtk-сессии по умолчанию – 15 минут. Не используемая gtk-сессия будет закрыта через 15 минут. Изменить таймаут или закрыть сессию по завершению обработки запроса возможно через установку свойств RestEXContext().

Создание новой сессии#

Происходит в случае, если:

  • в запрос не передан куки http сессии

  • или gtk сессия не существует в уникальном разрезе

    • Имя пользователя

    • База данных

    • Workspace

    • Пакет

При этом:

  • Создается gtk сессия

  • Вызываются методы прикладного пакета

    • onActivate

    • в соответствии с типом http запроса вызывается один из методов обработки запроса

      • get

      • post

  • В ответ добавляется кука http сессии

Работа в текущий сессии#

Происходит в случае, если:

  • в запрос передан куки http сессии

  • и gtk сессия существует в уникальном разрезе, и ее куки совпадает с переданным запросом

Таймаут сессии#

Происходит в случае, если:

  • в запрос передан куки не существующий http сессии

  • gtk сессии не существует

При этом

  • Происходит создание новой сессии и вызов метода обработки (смотри создание)

  • Возврат новой куки

Конфликт сессии#

Происходит в случае, если:

  • в запрос передан куки http сессии

  • gtk сессия существует в уникальном разрезе, и ее текущий куки не совпадает с куки http сессии

При этом

  • Генерируется ошибка захвата сессии

Захват сессии#

Происходит при условии:

  • в запрос не передан куки http сессии

  • или передан устаревший куки, который совпадает с куки в gtk сессии

  • gtk сессия существует в уникальном разрезе

При этом:

  • Вызываются методы прикладного пакета

    • onDeactivate

    • onActivate

  • в соответствии с типом http запроса вызывается один из методов обработки

  • Возвращается куки новой сессии

Доступ к данным REST-запроса из прикладного кода#

В прикладном коде Pkg-класса, выполняемом при REST-запросе, доступен объект RestESContext, предоставляющий доступ к данным REST-запроса и REST-ответа. Для получения контекста, выполните:

val restContextOpt = RestESContext()

Методы контекста:

  • sessionTimeout: Long
    Время жизни gtk-сессии после последнего запроса

  • request: HttpServletRequest
    Объект-запрос. Предоставляет доступ к параметрам, кукам и т.д.

  • responseBuilder: Response.ResponseBuilder
    Билдер ответа.

  • markSessionClose(): Unit
    Устанавливает флаг закрытия gtk-сессии по завершении обработки запроса

  • isSessionClose: Boolean
    Возвращает значение флага закрытия gtk-сессии

Обработка ошибок#

Коды ошибок#

  • 500 – server error
    Прикладное исключение

  • 409 - conflict
    Конфликт сессий

Формат ошибок#

При возникновении ошибки, по умолчанию, ответ будет возвращён в формате xml.

Для изменения формата ответа, передайте http-параметр format.

Доступные значения:

  • xml

  • json

Пример:

http://localhost:8080/app/sys/rest/es/pkg/Gs3_RestfulTestPkg/anypath?format=json
Xml ответ с ошибкой#
<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
<response >
  <status>%s</status>
  <error>
    <type>%s</type>
    <message>%s</message>
    <stacktrace><![CDATA[%s]]></stacktrace>
  </error>
</response>
Json ответ с ошибкой#
{
"response": {
    "status" : 0,
    "data":{},
    "error": {
        "type": "" ,
        "message": "",
        "stacktrace": ""
    }
}}

Пример обращения к сервису#

Java#

package ru.bitec.app.sys.rest;

import org.junit.Test;

import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Invocation;
import javax.ws.rs.core.Cookie;
import javax.ws.rs.core.Response;
import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;

public class PkgRestServiceTest0 {

    private static String rootAddress = "http://localhost:8080/sys/rest/es";
    private static Client client = ClientBuilder.newClient();
    private Path testPath = Paths.get(System.getProperty("user.dir"), "src\\test\\java\\ru\\bitec\\app\\sys\\rest");
    private Path cookiePath = testPath.resolve("JSESSIONID.cookie");


    @Test
    public void get_2_Cookie_test() throws Exception {
        String JSESSION_Cookie = load_JSESSIONID_Cookie();
        Invocation.Builder builder = client.target(rootAddress + "/pkg/Gs3_RestfulTestPkg/anypath").request()
                //.header("Authorization", "Basic " + Base64.encodeToString("admin:admin".getBytes("UTF-8"), false))
                .header("Authorization", "Basic " + java.util.Base64.getEncoder().encodeToString("admin:admin".getBytes("UTF-8")))
                .cookie(Cookie.valueOf(JSESSION_Cookie));
        Response response = builder.get();
        try {
            save_JSESSIONID_Cookie(response);
            String s = response.readEntity(String.class);
            System.out.println("response: " + s);
        } finally {
            response.close();
        }
    }

    private void save_JSESSIONID_Cookie(Response response) throws IOException {
        if (response.getCookies().containsKey("JSESSIONID")) {
            String cookie = response.getCookies().get("JSESSIONID").toString();
            Files.write(cookiePath, cookie.getBytes(Charset.defaultCharset()));
        }
    }

    private String load_JSESSIONID_Cookie() throws IOException {
        if (Files.exists(cookiePath))
            return new String(Files.readAllBytes(cookiePath), Charset.defaultCharset());
        else
            return null;
    }

}

REST-сервис для взаимодействия с пользовательскими сессиями#

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

Функциональность сервиса:

  1. Получение списка сессий, удовлетворяющих условиям поиска.

  2. Выполнение Jexl-скриптов в контексте гл.выборки приложения.

Получение списка сессий#

При отправке HTTP GET на адрес сервиса, будет возвращён JSON со списком сессий, удовлетворяющих условиям поиска. Http-запрос может содержать следующие параметры:

  • sbt
    Фильтр по имени SBT

  • clientId
    Фильтр по идентификатору клиента (этот идентификатор присваивается каждому экземпляру браузера и содержится в куках)

  • user
    Фильтр по имени пользователя

  • app
    Фильтр по имени главной выборки приложения (поиск осуществляется по вхождению переданного значения в полное имя гл.выбокрки)

Пример HTTP GET:

http://localhost:8080/app/sys/rest/sessions?sbt=test&user=admin&clientId=123456&app=Xxx_xxxxxx

Пример ответа:

{"sessions":[{
  "sid": "E1", 
  "id": "02b5f602-ac2c-4259-9452-18840a1cd124", 
  "user": "admin", 
  "clientId": "B54E2C1F-04E5-4BB2-9372-A9AB8E9C6676", 
  "database": "PGTEST", 
  "app": "gtk-ru.bitec.app.gs3.Gs3_TestXmlApplication"
}]}

Выполнение Jexl в контексте главной выборки приложения#

Простой HTTP POST#

При отправке HTTP POST на адрес сервиса, в контексте гл.выборки приложения сессии, с переданным идентификатором, будет выполнен Jexl-скрипт, переданный в теле Http POST.

Адреса сервисов:

  • http://{server:port}/app/sys/rest/sessions/{id}/jexl/mainsel
    Где {id} - Идентификатор сессии. Значение {id} можно получить из результата http get к сервису.

  • http://{server:port}/app/sys/rest/sessions/new/jexl/mainsel?appname={имя_гл_выборки}
    При этом будет запущена новая сессия. Для корректного запуска новой сессии и открытия приложения, необходимо передать имя главной выборки через http-параметр appname

Пример HTTP POST:

http://localhost:8080/app/sys/rest/sessions/949d77e9-80bf-4b93-bd3b-b424f8887783/jexl/mainsel

http://localhost:8080/app/sys/rest/sessions/new/jexl/mainsel?appname=Gs3_TestXmlApplication

В ответе вернётся JSON с результатом выполнения:

{
  "sessionId":"14849330-3196-4b33-80c5-caa370186720", 
  "result":"[результат выполнения Jexl]"
}
Пример на Java#
@Test
    public void post_test_1() throws Exception {
        Map<String, Object> rootMap = (Map<String, Object>) Json.parse(doTestGET(rootAddress + "?sbt=test"));
        List<Map<String, Object>> sessions = (List<Map<String, Object>>) rootMap.get("sessions");
        if (sessions.isEmpty())
            throw new Exception("No one ESession opened.");
        Map<String, Object> session = sessions.get(0);
        String id = (String) session.get("id");
        String rootAddress = "http://localhost:8080/app/sys/rest/sessions";
        doTestPOST(rootAddress + "/" + id + "/jexl/mainsel", "Some Jexl Script");
    }

    public String doTestGET(String uri, String database) throws Exception {
        System.out.println("HTTP GET: " + uri);
        Invocation.Builder builder = client.target(uri).request()
                //.header("Authorization", "Basic " + Base64.encodeToString("admin:admin".getBytes("UTF-8"), false))
                .header("Authorization", "Basic " + java.util.Base64.getEncoder().encodeToString("admin:admin".getBytes("UTF-8")));
        if (database != null) {
            builder.header("Database", database);
        }
        String result = "";
        Response response = builder.get();
        try {
            System.out.println("response status = " + response.getStatus());
            System.out.println("response MediaType = " + response.getMediaType());
            if (response.getStatus() == 200) {
                ByteArrayOutputStream out = new ByteArrayOutputStream();
                try (InputStream inputStream = response.readEntity(InputStream.class)) {
                    StreamHelper.copyStream(inputStream, out);
                }
                byte[] bytes = out.toByteArray();
                System.out.println("response: " + bytes.length + " bytes");
                result = new String(bytes);
                //System.out.println("filename: " + response.getHeaderString("Content-Disposition"));
            } else {
                result = response.readEntity(String.class);
            }
        } finally {
            response.close();
        }
        System.out.println(result);
        return result;
    }

    public String doTestPOST(String uri, String database, String body) throws Exception {
        System.out.println("HTTP POST: " + uri);
        Invocation.Builder builder = client.target(uri).request()
                //.header("Authorization", "Basic " + Base64.encodeToString("admin:admin".getBytes("UTF-8"), false))
                .header("Authorization", "Basic " + java.util.Base64.getEncoder().encodeToString("admin:admin".getBytes("UTF-8")));
        if (database != null) {
            builder.header("Database", database);
        }
        String result = "";
        Entity entity = Entity.entity(body, MediaType.TEXT_PLAIN_TYPE);
        Response response = builder.post(entity);
        try {
            System.out.println("response status = " + response.getStatus());
            System.out.println("response MediaType = " + response.getMediaType());
            if (response.getStatus() == 200) {
                ByteArrayOutputStream out = new ByteArrayOutputStream();
                try (InputStream inputStream = response.readEntity(InputStream.class)) {
                    StreamHelper.copyStream(inputStream, out);
                }
                byte[] bytes = out.toByteArray();
                System.out.println("response: " + bytes.length + " bytes");
                result = new String(bytes);
                //System.out.println("filename: " + response.getHeaderString("Content-Disposition"));
            } else {
                result = response.readEntity(String.class);
            }
        } finally {
            response.close();
        }
        System.out.println(result);
        return result;
    }

Form HTTP POST#

Альтернативным вариантом передачи Jexl скрипта, является отправка на адрес сервиса http формы, поле которой содержит Jexl-скрипт. Адреса сервисов:

  • http://{server:port}/app/sys/rest/form/sessions/{id}/jexl/mainsel

  • http://{server:port}/app/sys/rest/form/sessions/new/jexl/mainsel?appname={имя_гл_выборки}

В ответе вернётся команда перенаправления на страницу входа с идентификатором сессии в качестве http-параметра.

Пример HTML-страницы.#
<html>
   <body onload="onLoadFunc()">
    <script>
  function onLoadFunc() {
   const form = document.createElement('form');
   form.method = 'POST';
   form.id = 'MultilinePostForm'
   form.action = 'http://localhost:8080/app/sys/rest/form/sessions/new/jexl/mainsel?appname=Gs3_TestXmlApplication';
   const hiddenField = document.createElement('input');
   hiddenField.type = 'hidden';
   hiddenField.name = 'script';
   hiddenField.value = `Btk_ClassAvi.list().newForm().open();
   Gs3_RootTestAvi.list().newForm().open();`;
   form.appendChild(hiddenField);
   document.body.appendChild(form);
   form.submit();
  }
    </script>
   </body>
</html>

Сервис отчётов#

Сервис предназначен для формирования печатных документов, средствами сервера Global 3, по запросу из внешних систем.

Для получения файла со сформированным отчётом, необходимо отправить HTTP GET по адресу сервиса. Адреса сервисов:

  • http://{server:port}/app/sys/rest/report/{name}

  • http://{server:port}/app/sys/rest/report/{name}/{date}

где:

  • {name}
    Системное имя печатной формы

  • {date}
    Дата версии отчёта

Пример:

http://localhost:8080/app/sys/rest/report/Rpt_JasperSimpleQuery/15.02.2019

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

Читайте в разделе Аутентификация в REST/SOAP-сервисах.

Параметризация отчёта#

Для передачи параметров в формируемый отчёт, передавайте значения через http-параметры. Служебные символы и пробелы должны бить заменены Escape-символами.

Пример:

http://localhost:8080/app/sys/rest/report/Rpt_JasperSimpleQuery?param1=value1&param2=value2

Аутентификация в REST/SOAP сервисах#

Сервисы используют HTTP-аутентификацию двух типов: Basic и Bearer.

Basic - c использованием логина, пароля и имени базы#

При Basic-аутентификации, клиент должен передать в запросе:

  • Имя пользователя

  • Пароль

  • Имя базы

Имя пользователя и пароль передаются через HTTP-заголовок:

  • Authorization
    Значение: Basic {Base64Cred}
    где:
    {Base64Cred} – строка user:password, кодированная в Base64

Имя базы может быть передано через (в порядке приоритета):

  • Сегмент строки адреса. Пример: http://server/{DATABASE}/

  • HTTP-загловок Database со значением {DATABASE}.

  • HTTP-параметр Database со значением {DATABASE}. Пример: http://server/?Database={DATABASE}

где:
{DATABASE} - имя базы данных.

Если имя базы не передано ни одним из описанных способов, будет произведена попытка аутентификации с использованием имени базы по-умолчанию. База по умолчанию определяется из конфигурационного файла global3.config.xml (в порядке приоритета).

  • Значение атрибута <databases defaultDb="{DATABASE}"/>

  • Значение атрибута <database alias="{DATABASE}"/> первой базы в списке конфигураций <databases/>

Bearer - c использованием токена аутентификации#

При Bearer-аутентификации, клиент должен передать в запросе:

  • Токен аутентификации

    До версии AS 1.20 rc 15 включительно, доступны только токены сформрованные сервером приложений на основе: имени базы, имени пользователя и пароля.
    
  • Имя базы

    До версии AS 1.20 rc 15 включительно, указания имени базы при Bearer-аутентификации не требуется.
    

Запрос должен содержать HTTP-заголовок:

  • Authorization
    Значение: Bearer {Token}
    Где:
    {Token} – ключ авторизации, присвоенный пользователю. Можно получить в прикладном коде от сущности

    session.user.token: String
    

Имя базы может быть передано через (в порядке приоритета):

  • Сегмент строки адреса. Пример: http://server/{DATABASE}/

  • HTTP-загловок Database со значением {DATABASE}.

  • HTTP-параметр Database со значением {DATABASE}. Пример: http://server/?Database={DATABASE}

где:
{DATABASE} - имя базы данных.

Если имя базы не передано ни одним из описанных способов, будет произведена попытка аутентификации с использованием имени базы по-умолчанию. База по умолчанию определяется из конфигурационного файла global3.config.xml (в порядке приоритета).

  • Значение атрибута <databases defaultDb="{DATABASE}"/>

  • Значение атрибута <database alias="{DATABASE}"/> первой базы в списке конфигураций <databases/>

Типы токенов аутентификации#

Application Server Token#

Токен формируется сервером приложений после входа в приложение Глобал с использованием имени пользователя и пароля. Токен возвращется клиенту в Cookie access_token, привязанной к подмножеству адресов http[s]://server/{DATABASE}/. Эта Cookie автоматически присоединяется ко всем запросам по адресам http[s]://server/{DATABASE}/, выполняемым из браузера, аутентифицированного в системе Глобал.

Токен устаревает после перезапуска сервера приложений.

Gtk Json Web Token#

Токен, по стандарту JWT, формируется в модуле GTK (или на внешнем серсисе) и валидируется в модуле GTK.

GJWT разделяются на подтипы

Долгоживущий токен пользователя#

Формируется администратором системы Глобал и сообщается пользователю любым удобным способом, исключающим утечку токена.
Токен, сопоставленный с учётной записью пользователя, хранится в БД решения.
Срок годности - определяется администратором.

Тело JWT (payload) должно содержать следующие поля:

  • typ - "UserHash"

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

  • exp - дата устаревания

При проверке валидности токена, проверяется существование токена в списке сопоставленных с учётной записью токенов и дата устаревания.

Подписанный пользователем токен#

Формируется пользователем и подписывается закрытым RSA-ключом пользователя.
Открытый RSA-ключ, сопоставленный с учётной записью пользователя, хранится в БД решения.
Срок годности - определяется пользователем.

Тело JWT (payload) должно содержать следующие поля:

  • typ - "UserCrt"

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

  • cid - идентификатор открытого ключа в системе Глобал, сопоставленного с пользователем

  • exp - дата устаревания

При проверке валидности токена, на основе полей sub и cid из БД решения получается открытый RSA-ключ и проверяется валидность подписи JWT.

Подписанный прокси-пользователем токен#
Прокси-аутентификация - это аутентификация под именем пользователя ``№2`` от имени пользователя ``№1``,
именуемого ``прокси-пользователем``.

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

Формируется прокси-пользователем и подписывается закрытым RSA-ключом прокси-пользователя.
Открытый RSA-ключ, сопоставленный с учётной записью прокси-пользователя, хранится в БД решения.
Срок годности - определяется прокси-пользователем.

Тело JWT (payload) должно содержать следующие поля:

  • typ - "ProxyCrt"

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

  • psub - имя прокси-пользователя, которому принадлежитам закрытый и открыты ключи

  • cid - идентификатор открытого ключа в системе Глобал, сопоставленного с прокси-пользователем

  • exp - дата устаревания

При проверке валидности токена, на основе полей psub и cid, из БД решения получается открытый RSA-ключ прокси-пользователя и проверяется валидность подписи JWT.
Если токен валиден и пользователь psub имеет права на выполнение кода от имени других пользователей, запрос аутентифицируется под именем пользователя sub.

Пример запроса с аутентификацией по токену#

В примере используются библиотеки:

  • «io.jsonwebtoken» % «jjwt-api» % «0.10.8»,

  • «io.jsonwebtoken» % «jjwt-impl» % «0.10.8»,

  • «io.jsonwebtoken» % «jjwt-jackson» % «0.10.8»,

  • «org.apache.httpcomponents» % «httpclient» % «4.5.8»

import io.jsonwebtoken.Jwts;
import org.apache.http.HttpHeaders;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.junit.Assert;
import org.junit.Test;

import java.io.BufferedReader;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.util.Calendar;
import java.util.Date;
import java.util.stream.Collectors;

public class AuthProviderTest {

    private PrivateKey getPrivateKey(String proxyUser) throws NoSuchAlgorithmException {
        // Формируется рандомный закрытый ключ.
        // В рабочем коде необходимо использовать реальный закрытый ключ, принадлежащий пользователю.
        return KeyPairGenerator.getInstance("RSA").generateKeyPair().getPrivate();
    }

    @Test
    public void authenticate_with_gjwt_proxy_cert() throws Exception {
        Calendar calendar = Calendar.getInstance();
        calendar.add(Calendar.DAY_OF_MONTH, 7);
        String proxyUser = "proxy_user";
        String gjwt = "gjwt_" + Jwts.builder()
                .setAudience("GS")                  // Кому предназначен токен. Необязательный параметр.
                .setIssuer("Scheduler")             // Кем сформирован токен. Необязательный параметр.
                .setSubject("real_user")            // Имя пользователя, под которым необходимо аутентифицироваться.
                .claim("typ", "ProxyCrt")           // Подтип токена. В данном случае, токен для прокси-аутентификации.
                .claim("psub", proxyUser)           // Имя прокси-пользователя.
                .claim("cid", "123456789")          // Идентификатор открытого RSA-ключа прокси-пользователя в системе Глобал.
                .setExpiration(calendar.getTime())  // Дата устаревания токена.
                .signWith(getPrivateKey(proxyUser)) // Закрытый ключ прокси-пользователя, которым будет подписан токен.
                .compact();

        HttpGet httpGet = new HttpGet("http://localhost:8080/PGTEST/app/sys/rest/ss/pkg/Gs3_RestfulTestPkg/anypath");
        httpGet.addHeader(HttpHeaders.AUTHORIZATION, "Bearer " + gjwt);
        try (CloseableHttpClient client = HttpClients.createDefault()) {
            try (CloseableHttpResponse response = client.execute(httpGet)) {
                try (InputStream in = response.getEntity().getContent()) {
                    String s = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))
                            .lines()
                            .collect(Collectors.joining(System.lineSeparator()));
                    System.out.println(s);
                }
            }
        }
    }
}

Администрирование Rest-сервисов#

Класс Btk_AcPackage содержит информацию об администрируемых пакетах:

  • Имя пакета

  • id модуля, которому он принадлежит

  • является ли он rest-пакетом (bIsRest)

  • администрируются ли серверные полномочия (bControlServerPriv)

Пакет считается Rest-пакетом, если он наследуется от трейта RestPkg, в том числе от его потомков. Rest-пакеты регистрируются при обновлении администрируемых объектов по модулю. Если при обновлении окажется, что пакет больше не наследуется от RestPkg, то признак bIsRest в таблице будет снят.

Внимание

После изменения статуса администрируемости пакета, необходимо сбросить shared-кэш по классу Btk_AcPackage.

Выдача прав на вызов Rest-пакетов#

В карточке роли на закладке «Права на Rest-пакеты» отображен список пакетов из класса Btk_AcPackage и имеет ли роль доступ к ним. Информация по доступу для роли хранится в таблице Btk_AcRolePackagePriv. Для выбора\снятия доступа роли к пакету используйте чекбокс Имеет доступ. Во время индексации прав пользователей по роли для всех Rest-пакетов, у которых стоит галка «Контролировать серверные полномочия», будут выданы объектные привилегии пользователям, которые имеют данный профиль. Привилегии регистрируются на адм. объект Btk_AcPackage.

Примечание

Супер-пользователь имеет полный доступ

Администрирование SOAP-вызовов#

Администрирование SOAP-вызовов включается в Администратор \ Настройки > Настройки администрирования соответствующим чекбоксом. Для выдачи права роли необходимо дать доступ к объектной привилегии UseSoap адм. объекта Btk_ManagementPkg.

Примечание

Супер-пользователь имеет полный доступ