BI Consult Desktop Logo BI Consult Mobile Logo
  • Russian BI Исследование российских bi
  • Перейти на Fine BI
  • Контакты
  • +7 812 334-08-01
    +7 499 608-13-06
  • Отправить сообщение
  • Главная
  • Продукты Эксперт-BI
    • Дистрибуция
    • Розничная торговля
    • Производство
    • Операторы связи
    • Страхование
    • Банки
    • Лизинг
    • Логистика
    • Нефтегазовый сектор
    • Медицина
    • Сеть ресторанов
    • E-Commerce
    • Сельское хозяйство
    • Энергетика
    • FMCG
    • Девелоперы
    • Маркетплейсы
    • Пищевая промышленность
    • Фармацевтика
    • Построение Data Platform
    • Цифровая трансформация
    • Управление по KPI
    • Финансы
    • Продажи
    • Склад
    • HR
    • Маркетинг
    • Внутренний аудит
    • Категорийный менеджмент
    • S&OP и FP&A
    • Геоаналитика
    • Цепочки поставок (SCM)
    • AutoML
    • Process Mining
    • IBP
    • ИТ (CIO)
    • Закупки
  • Платформы
    • Системы бизнес-анализа (BI)
    • Интегрированное бизнес-планирование (IBP)
    • Хранилища данных (DWH / Lakehouse)
    • Каталоги данных (Data Catalog)
    • Системы ETL и ELT
    • AI / Исскуственный интеллект
    • Шина данных (ESB)
    • Система управления мастер-данными (MDM)
    • Семантический слой
  • Услуги
    • Переход на отечественные BI и DWH системы
    • Консалтинг
    • Пилотный проект
    • Обучение и сертификация
    • Бесплатное обучение
    • Поддержка
    • Технические задания
    • Сбор требований для проекта внедрения BI-системы
    • CI/CD для DWH
    • Аудит BI приложений и DWH
    • Выделенная команда
    • Настойка и поддержка баз данных
    • Разработка BI Стратегии
    • Styleguide для BI-системы
    • Как выбрать BI-систему
  • Курсы
    • Учебный курс Информационная грамотность (Data Literacy)
    • Учебный курс для бизнес-аналитиков
    • Учебный курс для системных аналитиков
    • Учебный курс по Data Governance
    • Учебный курс Как стать CDO
    • Учебный курс Современная архитектура хранилища данных
    • Учебный курс по Fine BI
    • Учебный курс по FineReport
    • Учебный курс по DWH
    • Учебный курс по Data Science (ML, AI)
    • Учебный курс по PostgreSQL
    • Учебный курс по Greenplum
    • Учебный курс по Apache Airflow и NiFi
    • Учебный курс по Open-source BI
    • Учебный курс по ClickHouse
    • Учебный курс по DataLens
    • Учебный курс по Loginom
    • Учебный курс по Modus BI и ETL
    • Учебный курс по Visiology
    • Учебный курс по dbt (Data Build Tool)
  • Компания
    • Руководство
    • Новости
    • Клиенты
    • Карьера
    • Скачать
    • Контакты

BI

  • FineBI
  • FineReport
  • FineDataLink
  • FineChatBI (FineAI)
  • Коннекторы данных из 1С в BI
  • Airflow / Nifi
  • Visiology
  • PIX BI
  • Modus BI
  • Yandex.DataLens
  • Open-source BI: Superset/Metabase
  • Luxms BI
  • AW BI + Alpha BI
  • FlyBI + Форсайт. Аналитическая Платформа
  • Loginom
  • Триафлай
  • AI / Исскуственный интеллект
  • Optimacros
  • Навигатор BI
  • Семантический слой

СУБД

  • Arenadata
  • ClickHouse
  • Greenplum
  • Postgres Professional
  • TData

Другое

  • Построение Data Platform
    • Аналитическое хранилище данных
    • Data Lake и Data Engineering
    • Подробнее про Data Lake
    • Внедрение Lakehouse
      • Apache Doris
      • StarRocks
      • Trino
    • Миграция витрин из пропиетарных DWH на новый стек
    • Учебный курс "Современная архитектура хранилища данных"
Главная » Курсы по системам бизнес-анализа и методологии » Учебный курс по PostgreSQL » Интеграция OAuth 2.0 для авторизации в PostgreSQL с использованием Keycloak

Интеграция OAuth 2.0 для авторизации в PostgreSQL с использованием Keycloak

Сегодня мы с удовольствием поделимся с вами ценнейшим опытом в области интеграции PostgreSQL с протоколом OAuth 2.0 через Keycloak.

 

 

 

OAuth 2.0 Device Authorization Flow представляет собой революционный подход к авторизации в системах управления базами данных. В отличие от традиционных методов аутентификации по паролю, которые несут в себе множество уязвимостей, OAuth 2.0 обеспечивает принципиально иной уровень безопасности. Внедрение этой технологии особенно актуально для облачных сред и микросервисных архитектур, где централизованное управление доступом становится критически важным.

Основные преимущества OAuth 2.0 перед стандартной аутентификацией очевидны. Во - первых, полностью исключается риск перехвата паролей, поскольку сам пароль никогда не передается клиенту. Во - вторых, появляется возможность централизованного управления учетными записями и политиками безопасности через единого провайдера. В - третьих, значительно упрощается процедура авторизации для пользователей, особенно в сценариях с ограниченными клиентскими возможностями.

Device Authorization Flow идеально подходит для использования в терминальных приложениях и автоматизированных сервисах, где пользователь может подтвердить доступ на отдельном устройстве через браузер или мобильное приложение. Это не только повышает безопасность, но и улучшает пользовательский опыт.

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

Процесс настройки Keycloak включает несколько критически важных этапов. Запуск осуществляется через Docker образ с созданием начального пользователя администратора.

docker run --name keycloak -p 8080:8080 -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:26.2.1 start-dev

 

Далее необходимо создать Realm - область настройки безопасности, которая включает учетные записи пользователей, роли, группы и настройки авторизации. Создание пользователей требует внимательного подхода к настройке учетных данных и атрибутов.

 

 

 

 

 

 

 

 

 

Далее переходим к созданию пользователей (Users).

Пользователи– это субъекты, которые могут входить в систему, они могут иметь связанные атрибуты ( электронная почта, имя пользователя, адрес и т.д.):

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Особое внимание стоит уделить созданию областей действия Client scopes, которые позволяют ограничить права доступа, объявляемые в токенах. Это делает токены более безопасными и управляемыми. Создание клиента Clients завершает базовую настройку Keycloak и требует точной конфигурации параметров аутентификации.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Переходя к настройке PostgreSQL, необходимо отметить несколько ключевых аспектов.

Создание ролей в PostgreSQL должно точно соответствовать пользователям в Keycloak. Роль — это сущность, которая может владеть объектами и иметь в базе определённые права:

CREATE ROLE alice;
ALTER ROLE alice WITH LOGIN;

 

Настройка файла postgresql.conf требует указания валидатора токенов OAuth.

oauth_validator_libraries = 'oauth_validator'

 

Критически важным является процесс сопоставления пользователей между Keycloak и PostgreSQL, который может осуществляться двумя способами: через файл pg ident conf или с помощью валидатора.

Через файл pg ident conf:

# MAPNAME    SYSTEM-USERNAME                           PG-USERNAME
oauthmap    "0fc72b6f-6221-4ed8-a916-069e7a081d14"     "alice"

 

Настройка файла pg hba conf завершает конфигурацию PostgreSQL и требует точного указания параметров OAuth, включая issuer и scope. Правильная настройка этого файла определяет безопасность всего соединения.

# TYPE  DATABASE        USER            ADDRESS                 METHOD
local   all             all             oauth issuer="http://192.168.0.156:8080/realms/postgres-realm/.well-known/openid-configuration" scope="openid postgres" map="oauthmap"

 

В 4 поле следует указать oauth и далее его параметры. В параметре issuer указываем URL дискавери-сервиса "http://192.168.0.156:8080/realms/postgres-realm/.well-known/openid-confi..., а в Scope указываем области доступа, которые будут запрашиваться у Keycloak для клиента.

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

Для сопоставления пользователей через pg_ident.conf добавляем параметр map, в котором указываем map id из файла pg_ident.conf, у нас это "oauthmap":

# TYPE  DATABASE        USER            ADDRESS                 METHOD
local   all             all             oauth issuer="http://192.168.0.156:8080/realms/postgres-realm/.well-known/openid-configuration" scope="openid postgres" map="oauthmap"

 

Сопоставление пользователей через валидатор - в этом случае вместро параметра map следует задать параметр delegate_ident_mapping=1.

# TYPE  DATABASE        USER            ADDRESS                 METHOD
local   all             all             oauth issuer="http://192.168.0.156:8080/realms/postgres-realm/.well-known/openid-configuration" scope="openid postgres" delegate_ident_mapping=1

 

Разработка валидатора токенов представляет собой наиболее сложную техническую задачу. Валидатор реализует набор обратных вызовов, которые вызываются сервером по мере необходимости для обработки запросов авторизации. Основная логика проверки включает разбор содержимого токена, извлечение полей sub и scope, сравнение разрешений и установку результата авторизации.

Реализация функции validate token требует глубокого понимания структуры JWT токенов и механизмов их проверки. Правильная обработка base64 кодирования и декодирования, анализ JSON структур и сравнение областей доступа все это критически важные компоненты успешной реализации.

 

Приступим к написанию  учебного валидатора:

oauth_validator.c

#include <string.h>
#include "postgres.h"
#include "token_utils.h"
#include "fmgr.h"
#include "libpq/oauth.h"
#include "miscadmin.h"
#include "nodes/pg_list.h"
#include "utils/builtins.h"
PG_MODULE_MAGIC;
/*
 * Объявления внутренних функций модуля.
 */
static void validator_startup(ValidatorModuleState *state);
static void validator_shutdown(ValidatorModuleState *state);
static bool validate_token(const ValidatorModuleState *state,
                           const char *token,
                           const char *role,
                           ValidatorModuleResult *result);
/*
 * Структура с указателями на функции обратных вызовов валидатора токенов OAuth.
 * PostgreSQL вызывает их в определённые моменты жизненного цикла модуля.
 */
static const OAuthValidatorCallbacks validator_callbacks = {
    PG_OAUTH_VALIDATOR_MAGIC, /* Магическое число для проверки версии API */
    .startup_cb = validator_startup,   /* Функция инициализации валидатора */
    .shutdown_cb = validator_shutdown, /* Функция завершения работы валидатора */
    .validate_cb = validate_token      /* Функция проверки токена */
};
/*
 * Точка входа в модуль валидатора OAuth.
 * PostgreSQL вызывает эту функцию при загрузке модуля.
 */
const OAuthValidatorCallbacks *
_PG_oauth_validator_module_init(void)
{
    return &validator_callbacks;
}
/*
 * Функция инициализации валидатора.
 * Вызывается один раз при старте работы модуля.
 */
static void
validator_startup(ValidatorModuleState *state)
{
    /*
     * Проверяем, совпадает ли версия сервера с той, с которой был собран модуль.
     */
    if (state->sversion != PG_VERSION_NUM)
        elog(ERROR, "oauth_validator: некорректная версия сервера: sversion=%d", state->sversion);
}
/*
 * Функция завершения работы валидатора.
 * Вызывается при выгрузке модуля или завершении работы сервера.
 */
static void
validator_shutdown(ValidatorModuleState *state)
{
    /* Пока ничего не делаем, но сюда можно добавить освобождение ресурсов при необходимости. */
}
/*
 * Основная функция проверки токена OAuth.
 *
 * Параметры:
 * - state: состояние модуля валидатора (может содержать конфигурацию и т.п.);
 * - token: строка с токеном, который нужно проверить;
 * - role: имя роли PostgreSQL, от имени которой пытается подключиться клиент;
 * - res: структура для возврата результата проверки.
 *
 * Возвращает true, если токен успешно проверен, иначе false.
 */
static bool
validate_token(const ValidatorModuleState *state,
               const char *token, const char *role,
               ValidatorModuleResult *res)
{
    char *sub = NULL;               /* Значение поля "sub" из токена (идентификатор пользователя) */
    char *scope = NULL;             /* Значение поля "scope" из токена (разрешённые области доступа) */
    const char *token_payload = NULL; /* Полезная нагрузка (payload) токена в виде строки JSON */
    List *granted_scopes = NIL;     /* Список областей доступа, полученных из токена */
    List *required_scopes = NIL;    /* Список обязательных областей доступа из HBA-конфигурации */
    bool matched = false;           /* Флаг успешного совпадения необходимых областей доступа */
    /* Инициализация результата */
    res->authn_id = NULL;   /* Идентификатор аутентификации (sub) */
    res->authorized = false; /* Флаг авторизации */
    /* Извлекаем полезную нагрузку из токена */
    token_payload = parse_token_payload(token);
    if (token_payload == NULL)
    {
        elog(LOG, "Неверный токен: отсутствует payload: %s", token);
        return false;
    }
    /* Извлекаем поля 'sub' и 'scope' из полезной нагрузки */
    extract_sub_scope_fields(token_payload, &sub, &scope);
    if (!sub || !scope)
    {
        elog(LOG, "Неверный токен: отсутствуют поля sub и/или scope: %s", token);
        return false;
    }
    /* Устанавливаем идентификатор пользователя (sub) в результат */
    res->authn_id = pstrdup(sub);
    /* Разбиваем список областей доступа из токена на элементы */
    granted_scopes = split_scopes(scope);
    /* Разбиваем обязательные области доступа из HBA-файла на элементы */
    required_scopes = split_scopes(MyProcPort->hba->oauth_scope);
    if (!granted_scopes || !required_scopes)
        return false;
    /* Проверяем, удовлетворяет ли токен обязательным областям доступа */
    matched = check_scopes(granted_scopes, required_scopes);
    /* Устанавливаем флаг авторизации */
    res->authorized = matched;
    return true;
}

 

token_utils.c/.h – функции-утилиты

token_utils.h

#ifndef TOKEN_UTILS_H
#define TOKEN_UTILS_H
#include <stdbool.h>
#include "common/jsonapi.h"
#include "nodes/pg_list.h"
const char* parse_token_payload(const char *token);
void extract_sub_scope_fields(const char *json, char **sub_field, char **scope_field);
const char *decode_base64(const char *b64);
char *base64url_to_base64(const char *b64url);
List *split_scopes(const char *raw);
bool check_scopes(List *granted, List *required);
#endif

 

token_utils.c

#include "PostgreSQL.h"
#include "token_utils.h"
#include "common/base64.h"
#include "mb/pg_wchar.h"
#define SUB_FIELD   0   /* Индекс для поля 'sub' */
#define SCOPE_FIELD 1   /* Индекс для поля 'scope' */
/*
 * Обработчик поля JSON-объекта.
 * Отмечает, что текущее обрабатываемое поле 'sub' и 'scope', чтобы сохранить их значения на следующем этапе обработки.
 */
static JsonParseErrorType
token_field_start(void *state, char *fname, bool isnull)
{
    char **fields = (char **) state;
    if (strcmp(fname, "sub") == 0)
        fields[SUB_FIELD] = (char *) 1;  /* Отметить, что обрабатываемое поле — это 'sub' */
    else if (strcmp(fname, "scope") == 0)
        fields[SCOPE_FIELD] = (char *) 1; /* Отметить, что обрабатываемое поле — это 'scope' */
    return JSON_SUCCESS;
}
/*
 * Обработчик значения JSON.
 * Сохраняет значение 'sub' или 'scope', если оно было отмечено ранее.
 */
static JsonParseErrorType
token_scalar(void *state, char *token, JsonTokenType tokentype)
{
    char **fields = (char **) state;
    if (fields[SUB_FIELD] == (char *) 1)
        fields[SUB_FIELD] = pstrdup(token);  /* Сохраняем значение 'sub' */
    else if (fields[SCOPE_FIELD] == (char *) 1)
        fields[SCOPE_FIELD] = pstrdup(token); /* Сохраняем значение 'scope' */
    return JSON_SUCCESS;
}
/*
 * Извлечение полей 'sub' и 'scope' из JSON строки.
 *
 * Параметры:
 *  - json: строка JSON
 *  - sub_field: возвращает значение поля 'sub'
 *  - scope_field: возвращает значение поля 'scope'
 */
void
extract_sub_scope_fields(const char *json, char **sub_field, char **scope_field)
{
    JsonLexContext lex;
    JsonSemAction sem;
    char **fields = palloc0(sizeof(char *) * 2); /* Выделяем память для 2 строк ('sub', 'scope') */
    *sub_field = NULL;
    *scope_field = NULL;
    /* Создаём лексический контекст для разбора JSON */
    makeJsonLexContextCstringLen(&lex, json, strlen(json), GetDatabaseEncoding(), true);
    /* Настраиваем обработчики JSON-парсера */
    memset(&sem, 0, sizeof(sem));
    sem.semstate = (void *) fields;
    sem.object_field_start = token_field_start;
    sem.scalar = token_scalar;
    /* Запускаем парсинг JSON */
    pg_parse_json(&lex, &sem);
    /* Возвращаем найденные значения */
    *sub_field = fields[SUB_FIELD];
    *scope_field = fields[SCOPE_FIELD];
}
/*
 * Извлекает payload из токена JWT.
 * Возвращает раскодированную строку payload в виде JSON.
 */
const char*
parse_token_payload(const char *token)
{
    char *dot1 = NULL;
    char *dot2 = NULL;
    int payload_len = 0;
    char *payload_b64url = NULL;
    char *b64 = NULL;
    if(!token)
        return NULL;
    /* Ищем первую и вторую точки в JWT (разделители header.payload.signature) */
    dot1 = strchr(token, '.');
    dot2 = dot1 ? strchr(dot1 + 1, '.') : NULL;
    if (!dot1 || !dot2)
    {
        elog(LOG, "Неверный формат токена, требуется две точки: %s", token);
        return NULL;
    }
    /* Извлекаем закодированный payload между точками */
    payload_len = dot2 - (dot1 + 1);
    payload_b64url = pnstrdup(dot1 + 1, payload_len);
    /* Преобразуем base64url в обычный base64 */
    b64 = base64url_to_base64(payload_b64url);
    /* Декодируем base64 в JSON строку */
    return decode_base64(b64);
}
/*
 * Преобразует строку в формате base64url в формат base64.
 * Заменяет символы '-' на '+', '_' на '/' и добавляет паддинг '=' при необходимости.
 */
char *
base64url_to_base64(const char *b64url)
{
    int len = strlen(b64url);
    int pad = (4 - (len % 4)) % 4; /* Определяем количество символов '=' для паддинга */
    char *b64 = palloc(len + pad + 1);
    for (int i = 0; i < len; i++)
    {
        if (b64url[i] == '-')
            b64[i] = '+';
        else if (b64url[i] == '_')
            b64[i] = '/';
        else
            b64[i] = b64url[i];
    }
    /* Добавляем паддинг '=' */
    for (int i = 0; i < pad; i++)
        b64[len + i] = '=';
    b64[len + pad] = '\0';
    return b64;
}
/*
 * Декодирует строку base64 в обычную строку.
 * Возвращает раскодированную строку или NULL в случае ошибки.
 */
const char *
decode_base64(const char *b64)
{
    int encoded_len = strlen(b64);
    int max_decoded_len = pg_b64_dec_len(encoded_len); /* Вычисляем необходимую длину буфера */
    char *decoded = palloc(max_decoded_len + 1);
    int decoded_len = pg_b64_decode(b64, encoded_len, decoded, max_decoded_len);
    if (decoded_len <= 0)
    {
        elog(LOG, "Неверный формат токена: ошибка декодирования base64");
        return NULL;
    }
    decoded[decoded_len] = '\0';
    return decoded;
}
/*
 * Разбивает строку с пробелами (например, список scope из токена) на список строк List.
 */
List *
split_scopes(const char *raw)
{
    List *result = NIL;
    char *str = pstrdup(raw);  /* Делаем копию строки, так как strtok её изменяет */
    char *tok = strtok(str, " ");
    while (tok)
    {
        result = lappend(result, pstrdup(tok));
        tok = strtok(NULL, " ");
    }
    return result;
}
/*
 * Функция сравнения строк для сортировки списка.
 */
static int
list_string_cmp(const ListCell *a, const ListCell *b)
{
    const char *sa = (const char *) lfirst(a);
    const char *sb = (const char *) lfirst(b);
    return strcmp(sa, sb);
}
/*
 * Проверяет, содержатся ли все обязательные области доступа (required) в предоставленных (granted).
 * Списки предварительно сортируются.
 *
 * Возвращает true, если все required scopes найдены в granted scopes.
 */
bool
check_scopes(List *granted, List *required)
{
    ListCell *gcell;
    ListCell *rcell;
    /* Сортируем оба списка для упрощения сравнения */
    list_sort(granted, list_string_cmp);
    list_sort(required, list_string_cmp);
    gcell = list_head(granted);
    rcell = list_head(required);
    while (rcell != NULL && gcell != NULL)
    {
        char *r = (char *) lfirst(rcell);
        char *g = (char *) lfirst(gcell);
        int cmp = strcmp(r, g);
        if (cmp == 0)
        {
            /* Найдено совпадение — переходим к следующему элементу required */
            rcell = lnext(required, rcell);
            gcell = lnext(granted, gcell);
        }
        else if (cmp > 0)
        {
            /* granted "отстаёт" — переходим к следующему элементу granted */
            gcell = lnext(granted, gcell);
        }
        else
        {
            /* required элемент не найден в granted — возвращаем false */
            return false;
        }
    }
    /* Если не все элементы required были найдены — ошибка */
    if (rcell != NULL)
        return false;
    return true;
}

 

Makefile

# contrib/oauth_validator/Makefile
PGFILEDESC = "oauth_validator - OAuth validator"
MODULE_big = oauth_validator
OBJS = \
                $(WIN32RES) \
                oauth_validator.o \
                token_utils.o
PG_CPPFLAGS += -I$(top_srcdir)/src/common
PG_CPPFLAGS += -I$(libpq_srcdir)
PG_CONFIG = pg_config
PGXS := $(shell $(PG_CONFIG) --pgxs)
include $(PGXS)

 

Теперь рассмотрим обратные вызовы.

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

typedef void (*ValidatorStartupCB) (ValidatorModuleState *state);

 

ValidatorStartupCB startup_cb;

 

validate_cb выполняется в том случае, когда пользователь пытается пройти авторизацию с помощью OAuth. Любое состояние, установленное в предыдущих вызовах, будет доступно в state->private_data:

typedef bool (*ValidatorValidateCB) (const ValidatorModuleState *state,
                                     const char *token, const char *role,
                                     ValidatorModuleResult *result);
ValidatorValidateCB validate_cb;

 

Аргумент token содержит токен-носитель для проверки. PostgreSQL позаботился о том, чтобы синтаксически он был правильно сформирован. Параметр role содержит роль, от имени которой пользователь запросил вход в систему. Обратный вызов должен задать выходные параметры в результирующей структуре:

typedef struct ValidatorModuleResult
{
    bool        authorized;
    char       *authn_id;
} ValidatorModuleResult;

 

Соединение будет установлено только в случае, если валидатор установит для параметра result->authorized значение true.

В случае неудачной проверки токена валидатор должен возвращать значение false.

Обратный вызов shutdown_cb выполняется, когда существует серверный процесс, связанный с подключением. Если валидатор имеет какое-либо выделенное состояние, обратный вызов должен освободить его, чтобы избежать утечки ресурсов:

typedef void (*ValidatorShutdownCB) (ValidatorModuleState *state);
ValidatorShutdownCB shutdown_cb;

 

Процесс авторизации

Начнем с настройки логирования. Чтобы посмотреть, какие запросы отправляет PostgreSQL и какие ответы получает, в postgresql.conf можно включить логирование запросов:

log_connections = on

 

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

  • префикс ">" означает запрос в Keycloak
  • префикс "<" означает ответ Keycloak

 

discovery
[libcurl] *   Trying 192.168.0.156:8080...
[libcurl] * Connected to 192.168.0.156 (192.168.0.156) port 8080 (#0)
[libcurl] > GET /realms/postgres-realm/.well-known/openid-configuration HTTP/1.1
[libcurl] > Host: 192.168.0.156:8080
[libcurl] >
[libcurl] < HTTP/1.1 200 OK
[libcurl] < content-length: 6638
[libcurl] < Cache-Control: no-cache, must-revalidate, no-transform, no-store
[libcurl] < Content-Type: application/json;charset=UTF-8
[libcurl] < Referrer-Policy: no-referrer
[libcurl] < Strict-Transport-Security: max-age=31536000; includeSubDomains
[libcurl] < X-Content-Type-Options: nosniff
[libcurl] < X-Frame-Options: SAMEORIGIN
[libcurl] <
[libcurl] < {"issuer":"http://192.168.0.156:8080/realms/postgres-realm","authorization_endpoint":"http://192.168.0.156:8080/realms/postgres-realm/protocol/openid-connect/auth","token_endpoint":"http://192.168.0.156:8080/realms/postgres-realm/protocol/openid-connect/token","introspection_endpoint":"http://192.168.0.156:8080/realms/postgres-realm/protocol/openid-connect/token/introspect","userinfo_endpoint":"http://192.168.0.156:8080/realms/postgres-realm/protocol/openid-connect/userinfo","end_session_endpoint":"http://192.168.0.156:8080/realms/postgres-realm/protocol/openid-connect/logout","frontchannel_logout_session_supported":true,"frontchannel_logout_supported":true,"jwks_uri":"http://192.168.0.156:8080/realms/postgres-realm/protocol/openid-connect/certs","check_session_iframe":"http://192.168.0.156:8080/realms/postgres-realm/protocol/openid-connect/login-status-iframe.html","grant_types_supported":["authorization_code","client_credentials","implicit","password","refresh_token","urn:ietf:params:oauth:grant-type:device_code","urn:ietf:params:oauth:grant-type:token-exchange","urn:ietf:params:oauth:grant-type:uma-ticket","urn:openid:params:grant-type:ciba"],"acr_values_supported":["0","1"],"response_types_supported":["code","none","id_token","token","id_token token","code id_token","code token","code id_token token"],"subject_types_supported":["public","pairwise"],"prompt_values_supported":["none","login","consent"],"id_token_signing_alg_values_supported":["PS384","RS384","EdDSA","ES384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"id_token_encryption_alg_values_supported":["ECDH-ES+A256KW","ECDH-ES+A192KW","ECDH-ES+A128KW","RSA-OAEP","RSA-OAEP-256","RSA1_5","ECDH-ES"],"id_token_encryption_enc_values_supported":["A256GCM","A192GCM","A128GCM","A128CBC-HS256","A192CBC-HS384","A256CBC-HS512"],"userinfo_signing_alg_values_supported":["PS384","RS384","EdDSA","ES384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512","none"],"userinfo_encryption_alg_values_supported":["ECDH-ES+A256KW","ECDH-ES+A192KW","ECDH-ES+A128KW","RSA-OAEP","RSA-OAEP-256","RSA1_5","ECDH-ES"],"userinfo_encryption_enc_values_supported":["A256GCM","A192GCM","A128GCM","A128CBC-HS256","A192CBC-HS384","A256CBC-HS512"],"request_object_signing_alg_values_supported":["PS384","RS384","EdDSA","ES384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512","none"],"request_object_encryption_alg_values_supported":["ECDH-ES+A256KW","ECDH-ES+A192KW","ECDH-ES+A128KW","RSA-OAEP","RSA-OAEP-256","RSA1_5","ECDH-ES"],"request_object_encryption_enc_values_supported":["A256GCM","A192GCM","A128GCM","A128CBC-HS256","A192CBC-HS384","A256CBC-HS512"],"response_modes_supported":["query","fragment","form_post","query.jwt","fragment.jwt","form_post.jwt","jwt"],"registration_endpoint":"http://192.168.0.156:8080/realms/postgres-realm/clients-registrations/openid-connect","token_endpoint_auth_methods_supported":["private_key_jwt","client_secret_basic","client_secret_post","tls_client_auth","client_secret_jwt"],"token_endpoint_auth_signing_alg_values_supported":["PS384","RS384","EdDSA","ES384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"introspection_endpoint_auth_methods_supported":["private_key_jwt","client_secret_basic","client_secret_post","tls_client_auth","client_secret_jwt"],"introspection_endpoint_auth_signing_alg_values_supported":["PS384","RS384","EdDSA","ES384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"authorization_signing_alg_values_supported":["PS384","RS384","EdDSA","ES384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"authorization_encryption_alg_values_supported":["ECDH-ES+A256KW","ECDH-ES+A192KW","ECDH-ES+A128KW","RSA-OAEP","RSA-OAEP-256","RSA1_5","ECDH-ES"],"authorization_encryption_enc_values_supported":["A256GCM","A192GCM","A128GCM","A128CBC-HS256","A192CBC-HS384","A256CBC-HS512"],"claims_supported":["aud","sub","iss","auth_time","name","given_name","family_name","preferred_username","email","acr"],"claim_types_supported":["normal"],"claims_parameter_supported":true,"scopes_supported":["openid","offline_access","organization","service_account","postgres","address","phone","acr","profile","microprofile-jwt","web-origins","roles","basic","email"],"request_parameter_supported":true,"request_uri_parameter_supported":true,"require_request_uri_registration":true,"code_challenge_methods_supported":["plain","S256"],"tls_client_certificate_bound_access_tokens":true,"revocation_endpoint":"http://192.168.0.156:8080/realms/postgres-realm/protocol/openid-connect/revoke","revocation_endpoint_auth_methods_supported":["private_key_jwt","client_secret_basic","client_secret_post","tls_client_auth","client_secret_jwt"],"revocation_endpoint_auth_signing_alg_values_supported":["PS384","RS384","EdDSA","ES384","HS256","HS512","ES256","RS256","HS384","ES512","PS256","PS512","RS512"],"backchannel_logout_supported":true,"backchannel_logout_session_supported":true,"device_authorization_endpoint":"http://192.168.0.156:8080/realms/postgres-realm/protocol/openid-connect/auth/device","backchannel_token_delivery_modes_supported":["poll","ping"],"backchannel_authentication_endpoint":"http://192.168.0.156:8080/realms/postgres-realm/protocol/openid-connect/ext/ciba/auth","backchannel_authentication_request_signing_alg_values_supported":["PS384","RS384","EdDSA","ES384","ES256","RS256","ES512","PS256","PS512","RS512"],"require_pushed_authorization_requests":false,"pushed_authorization_request_endpoint":"http://192.168.0.156:8080/realms/postgres-realm/protocol/openid-connect/ext/par/request","mtls_endpoint_aliases":{"token_endpoint":"http://192.168.0.156:8080/realms/postgres-realm/protocol/openid-connect/token","revocation_endpoint":"http://192.168.0.156:8080/realms/postgres-realm/protocol/openid-connect/revoke","introspection_endpoint":"http://192.168.0.156:8080/realms/postgres-realm/protocol/openid-connect/token/introspect","device_authorization_endpoint":"http://192.168.0.156:8080/realms/postgres-realm/protocol/openid-connect/auth/device","registration_endpoint":"http://192.168.0.156:8080/realms/postgres-realm/clients-registrations/openid-connect","userinfo_endpoint":"http://192.168.0.156:8080/realms/postgres-realm/protocol/openid-connect/userinfo","pushed_authorization_request_endpoint":"http://192.168.0.156:8080/realms/postgres-realm/protocol/openid-connect/ext/par/request","backchannel_authentication_endpoint":"http://192.168.0.156:8080/realms/postgres-realm/protocol/openid-connect/ext/ciba/auth"},"authorization_response_iss_parameter_supported":true}

 

auth device
[libcurl] * Connection #0 to host 192.168.0.156 left intact
[libcurl] * Found bundle for host: 0x6124961fd400 [serially]
[libcurl] * Can not multiplex, even if we wanted to
[libcurl] * Re-using existing connection #0 with host 192.168.0.156
[libcurl] * Server auth using Basic with user 'postgres-client'
[libcurl] > POST /realms/postgres-realm/protocol/openid-connect/auth/device HTTP/1.1
[libcurl] > Host: 192.168.0.156:8080
[libcurl] > Authorization: Basic cG9zdGdyZXMtY2xpZW50OmZTY1hYcDFUcFNQY3BVaEZLcWk0eDk4alZ5NTR1Y1RC
[libcurl] > Content-Length: 47
[libcurl] > Content-Type: application/x-www-form-urlencoded
[libcurl] >
[libcurl] > scope=openid+postgres&client_id=postgres-client
[libcurl] < HTTP/1.1 200 OK
[libcurl] < Cache-Control: no-store, must-revalidate, max-age=0
[libcurl] < content-length: 296
[libcurl] < Content-Type: application/json
[libcurl] < Referrer-Policy: no-referrer
[libcurl] < Strict-Transport-Security: max-age=31536000; includeSubDomains
[libcurl] < X-Content-Type-Options: nosniff
[libcurl] < X-Frame-Options: SAMEORIGIN
[libcurl] <
[libcurl] < {"device_code":"tgElqUogevqjEkZy6-Z1i209pgoKW9_CT0t9wwNjafY","user_code":"WXAI-ZNVY","verification_uri":"http://192.168.0.156:8080/realms/postgres-realm/device","verification_uri_complete":"http://192.168.0.156:8080/realms/postgres-realm/device?user_code=WXAI-ZNVY","expires_in":600,"interval":5}
token
Клиент ожидает одобрения запроса авторизации пользователя:
[libcurl] * Connection #0 to host 192.168.0.156 left intact
[libcurl] * Found bundle for host: 0x6124961fd400 [serially]
[libcurl] * Can not multiplex, even if we wanted to
[libcurl] * Re-using existing connection #0 with host 192.168.0.156
[libcurl] * Server auth using Basic with user 'postgres-client'
[libcurl] > POST /realms/postgres-realm/protocol/openid-connect/token HTTP/1.1
[libcurl] > Host: 192.168.0.156:8080
[libcurl] > Authorization: Basic cG9zdGdyZXMtY2xpZW50OmZTY1hYcDFUcFNQY3BVaEZLcWk0eDk4alZ5NTR1Y1RC
[libcurl] > Content-Length: 147
[libcurl] > Content-Type: application/x-www-form-urlencoded
[libcurl] >
[libcurl] > device_code=tgElqUogevqjEkZy6-Z1i209pgoKW9_CT0t9wwNjafY&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code&client_id=postgres-client
[libcurl] < HTTP/1.1 400 Bad Request
[libcurl] < Cache-Control: no-store
[libcurl] < Pragma: no-cache
[libcurl] < content-length: 98
[libcurl] < Content-Type: application/json
[libcurl] < Referrer-Policy: no-referrer
[libcurl] < Strict-Transport-Security: max-age=31536000; includeSubDomains
[libcurl] < X-Content-Type-Options: nosniff
[libcurl] < X-Frame-Options: SAMEORIGIN
[libcurl] <
[libcurl] < {"error":"authorization_pending","error_description":"The authorization request is still pending"}

 

Пользователь одобрен:

[libcurl] * Connection #0 to host 192.168.0.156 left intact
[libcurl] * Found bundle for host: 0x6124961fd400 [serially]
[libcurl] * Can not multiplex, even if we wanted to
[libcurl] * Re-using existing connection #0 with host 192.168.0.156
[libcurl] * Server auth using Basic with user 'postgres-client'
[libcurl] > POST /realms/postgres-realm/protocol/openid-connect/token HTTP/1.1
[libcurl] > Host: 192.168.0.156:8080
[libcurl] > Authorization: Basic cG9zdGdyZXMtY2xpZW50OmZTY1hYcDFUcFNQY3BVaEZLcWk0eDk4alZ5NTR1Y1RC
[libcurl] > Content-Length: 147
[libcurl] > Content-Type: application/x-www-form-urlencoded
[libcurl] >
[libcurl] > device_code=tgElqUogevqjEkZy6-Z1i209pgoKW9_CT0t9wwNjafY&grant_type=urn%3Aietf%3Aparams%3Aoauth%3Agrant-type%3Adevice_code&client_id=postgres-client
[libcurl] < HTTP/1.1 200 OK
[libcurl] < Cache-Control: no-store
[libcurl] < Pragma: no-cache
[libcurl] < content-length: 3307
[libcurl] < Content-Type: application/json
[libcurl] < Referrer-Policy: no-referrer
[libcurl] < Strict-Transport-Security: max-age=31536000; includeSubDomains
[libcurl] < X-Content-Type-Options: nosniff
[libcurl] < X-Frame-Options: SAMEORIGIN
[libcurl] <
[libcurl] < {"access_token":"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIwb1RQSV85LXVIQWJ5Q0t3M2luTm5MTW5hU21hc05nWS1OaDVkTzJ4X0lNIn0.eyJleHAiOjE3NDYwMzEyNTMsImlhdCI6MTc0NjAzMDk1MywiYXV0aF90aW1lIjoxNzQ2MDMwOTUyLCJqdGkiOiJvbnJ0ZGc6MWU3MmRlNGUtNTRhYi00OTZjLWIxYWYtY2FhYmRiYzJlYzExIiwiaXNzIjoiaHR0cDovLzE5Mi4xNjguMC4xNTY6ODA4MC9yZWFsbXMvcG9zdGdyZXMtcmVhbG0iLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMGZjNzJiNmYtNjIyMS00ZWQ4LWE5MTYtMDY5ZTdhMDgxZDE0IiwidHlwIjoiQmVhcmVyIiwiYXpwIjoicG9zdGdyZXMtY2xpZW50Iiwic2lkIjoiNjRlMDUzMTMtM2UyMi00MmNjLWE0YmItOTAxODU2YTFhYjMzIiwiYWxsb3dlZC1vcmlnaW5zIjpbIi8qIl0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJvZmZsaW5lX2FjY2VzcyIsImRlZmF1bHQtcm9sZXMtcG9zdGdyZXMtcmVhbG0iLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoib3BlbmlkIHByb2ZpbGUgcG9zdGdyZXMiLCJuYW1lIjoiYWxpY2UgcG9zdGdyZXMiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhbGljZSIsImdpdmVuX25hbWUiOiJhbGljZSIsImZhbWlseV9uYW1lIjoicG9zdGdyZXMifQ.RXMszI-snIdrXyyTw74U8QXQeDG3zpfV4OvxYuJQvsb86eauXkKHAH35GfEm3XvQbtmpdSdfs1S4i11d69dUjpVTgPpzx6G7IXCXj2NTowzZuyuvdnLxPi1aXdxXqOKNSLSj5PXhGIaZhWsn2sR8dAJ0jjWTUO_lh8qJuJYaDcFulWn_flHVGQYzMZ5PTneRadg8h_1dWp4HSr6yC74NmF94dnOBmytivM4a__Wcq6TkZ3KLn_gafqnn72HpWY0WRwyZdQuzc5o8mE3UUAoKukxMnwDG7Yhxif2YFb_a5aCloMbL9aDghbMypahl3MiJHHx3j50FavSRm0FJa3zK9w","expires_in":300,"refresh_expires_in":1800,"refresh_token":"eyJhbGciOiJIUzUxMiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJjOGNiNjY3Ni00OTAxLTRmNjItOTI0OS1kMzY2MWI5Mjg3OTIifQ.eyJleHAiOjE3NDYwMzI3NTMsImlhdCI6MTc0NjAzMDk1MywianRpIjoiZTJkNzkzODUtNjBhZS00MTIwLWIwODAtNjVmYWU4ZmNhYzIzIiwiaXNzIjoiaHR0cDovLzE5Mi4xNjguMC4xNTY6ODA4MC9yZWFsbXMvcG9zdGdyZXMtcmVhbG0iLCJhdWQiOiJodHRwOi8vMTkyLjE2OC4wLjE1Njo4MDgwL3JlYWxtcy9wb3N0Z3Jlcy1yZWFsbSIsInN1YiI6IjBmYzcyYjZmLTYyMjEtNGVkOC1hOTE2LTA2OWU3YTA4MWQxNCIsInR5cCI6IlJlZnJlc2giLCJhenAiOiJwb3N0Z3Jlcy1jbGllbnQiLCJzaWQiOiI2NGUwNTMxMy0zZTIyLTQyY2MtYTRiYi05MDE4NTZhMWFiMzMiLCJzY29wZSI6Im9wZW5pZCBwcm9maWxlIHdlYi1vcmlnaW5zIHBvc3RncmVzIHJvbGVzIGJhc2ljIn0.43pRSq4PBO7ZY86jt8dIL7xZJylntY_CZXllcRfwfh41IRCOft6iqIWdJQp7TJv_JIDI-_-QeOSf1EC_wzABNg","token_type":"Bearer","id_token":"eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICIwb1RQSV85LXVIQWJ5Q0t3M2luTm5MTW5hU21hc05nWS1OaDVkTzJ4X0lNIn0.eyJleHAiOjE3NDYwMzEyNTMsImlhdCI6MTc0NjAzMDk1MywiYXV0aF90aW1lIjoxNzQ2MDMwOTUyLCJqdGkiOiIzNWM3ZDAyZC05OGFjLTQzMTgtOTg3NC0zYzY0Mjg5NjFhMjgiLCJpc3MiOiJodHRwOi8vMTkyLjE2OC4wLjE1Njo4MDgwL3JlYWxtcy9wb3N0Z3Jlcy1yZWFsbSIsImF1ZCI6InBvc3RncmVzLWNsaWVudCIsInN1YiI6IjBmYzcyYjZmLTYyMjEtNGVkOC1hOTE2LTA2OWU3YTA4MWQxNCIsInR5cCI6IklEIiwiYXpwIjoicG9zdGdyZXMtY2xpZW50Iiwic2lkIjoiNjRlMDUzMTMtM2UyMi00MmNjLWE0YmItOTAxODU2YTFhYjMzIiwiYXRfaGFzaCI6ImphOXRKZ1E0VkVPTTNBZGc0VWJBVGciLCJuYW1lIjoiYWxpY2UgcG9zdGdyZXMiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhbGljZSIsImdpdmVuX25hbWUiOiJhbGljZSIsImZhbWlseV9uYW1lIjoicG9zdGdyZXMifQ.dH5hM21-ygBiCXoA9NVOou-L3esUVJUFFUmt_1fU0jc9al4Lk7EUN-cqHicHZPD48HhkIWMJK7WZjxnZLDlkG7ORGdDKPGccMU4-sGsRuVu-GDuNFo_5kHAh_NJZsLBXz9UkpNBkd8ROxK3-fbmyYdwsuwNeg6KNhSOj0FxEnxLc0-HrjEE92P7hzq0PD29oY2jhRKcqpbtknMwxFkkMBi8xPgdpuyTmLtJD3-xxYuwMKP7WUGzwzVAFqfrhFm5O5dJxeld5fTFE4Kyl9fR24JcjtfxBeIHVJqLiQkl9Et_KNGiFoXDG4Xwcc7eIUaBnauhY5_froYvKS8NbQxCOUg","not-before-policy":0,"session_state":"64e05313-3e22-42cc-a4bb-901856a1ab33","scope":"openid profile postgres"}

 

Авторизация через psql. Для целей тестирования разрешим авторизацию по незащищённому протоколу http:

export PGOAUTHDEBUG="UNSAFE"

 

Запустим psql и укажем детали подключения:

​psql "user=alice dbname=postgres oauth_issuer=http://192.168.0.156:8080/realms/postgres-realm/.well-known/openid-configuration oauth_client_id=postgres-client oauth_client_secret=YYi8LqfzHRMnqUUlptpWVC2k7eWNrqjX"

 

Мы увидим URL и код, который нужно будет ввести после перехода на этот URL: 

Вводим его в браузере http://192.168.0.156:8080/realms/postgres-realm/device:

 

 

 

 

 

 

 

 

 

 

 

 

 

теперь можем вводить команды в терминале:

psql (18devel)
Type "help" for help.
postgres=>

 

Мы успешно авторизовались в PostgreSQL!

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

Ошибки в конфигурации Client scopes в Keycloak могут привести к созданию токенов с недостаточными или избыточными правами. В первом случае пользователи не смогут получить доступ к необходимым ресурсам, во втором возникает риск несанкционированного доступа к чувствительным данным.

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

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

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

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

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

При правильной реализации система обеспечивает беспрецедентный уровень безопасности и удобства. Централизованное управление доступом позволяет быстро реагировать на изменения в составе сотрудников и их обязанностях. Единая точка управления политиками безопасности значительно снижает операционные расходы.

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

Наша компания готова предоставить полный спектр услуг по внедрению и сопровождению систем аутентификации на основе OAuth 2.0 для PostgreSQL. Мы предлагаем не только техническую реализацию, но и комплексный подход, включающий аудит безопасности, проектирование архитектуры, реализацию и последующую поддержку.

Опыт нашей команды позволяет гарантировать надежность и безопасность внедряемых решений. Мы учитываем все возможные риски и обеспечиваем соответствие системы требованиям конкретной организации. Правильно реализованная система авторизации OAuth 2.0 не только повышает безопасность, но и становится конкурентным преимуществом, позволяя организации гибко управлять доступом и быстро адаптироваться к изменениям.

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

 

Узнать стоимость решенияЗапросить видео презентацию

← Предыдущая статья
Как оптимизировать таблицы в Postgres
Следующая статья →
Построение и оптимизация планов выполнения запросов в PostgreSQL: теория, архитектура и прикладные методы диагностики и настройки
Запросить видео презентацию Запросить доступ к демо стенду online Узнать стоимость лицензий

Задать вопрос

loading...

Решения

Анализировать ФинансыУвеличивайте ПродажиОптимальный Склад и ЛогистикаМаркетинговые Метрики

Клиенты
  • Группа компаний «Невский кондитер» основана в 1996 году в Санкт-Петербурге и на сегодняшний день является одним из крупнейших производителей кондитерских изделий в России.

     

  • Компания ООО "Комус" - один из лидеров российского рынка оптовых продаж офисных товаров и техники. Компания поставляет широкий ассортимент продукции - от канцелярских принадлежностей до компьютерной техники и офисной мебели.

  • Ситилинк

    Электронный дискаунтер «Ситилинк» — один из крупнейших онлайн‑ритейлеров России (3‑е место по объему онлайн‑продаж в рейтинге Data Insight и Ruward 2016 года E‑commerce Index TOP‑100, 8 место в рейтинге Forbes «20 самых дорогих компаний Рунета — 2017»). На рынке работает 9 лет.

    В ассортименте дискаунтера более 50 000 наименований компьютерной цифровой, бытовой и садовой техники, офисной мебели и других товарных категорий. Более 700 мировых брендов в портфеле. Около 4 000 сотрудников по всей России

  • KERAMA MARAZZI — международный бренд, входящий в число лидеров глобального рынка керамики. Бизнес компании охватывает весь процесс создания керамических изделий, от глиняных карьеров до фирменной розницы во всех крупных городах РФ и за рубежом.

  • Решения
    • Дистрибуция
    • Розничная торговля
    • Производство
    • Операторы связи
    • Страхование
    • Банки
    • Лизинг
    • Логистика
    • Нефтегазовый сектор
    • Медицина
    • Сеть ресторанов
    • E-Commerce
    • Энергетика
    • Фармацевтика
  • Услуги
    • Переход на отечественные BI и DWH
    • Консалтинг
    • Пилотный проект
    • Обучение и сертификация
    • Бесплатное обучение
    • Техническая поддержка
    • Технические задания
    • Сбор требований для проекта внедрения BI-системы
    • CI/CD для DWH
    • Аудит BI приложений
    • Выделенная команда
    • Настойка и поддержка баз данных
    • Разработка BI Стратегии
    • Styleguide для BI-системы
    • Как выбрать BI-систему
  • Платформы
    • FineBI
    • FineReport
    • FineDataLink
    • Коннекторы данных из 1С в BI
    • Airflow + NiFi
    • Visiology
    • Luxms BI
    • Modus BI
    • PIX BI
    • Arenadata
    • ClickHouse
    • Greenplum
    • Postgres Professional
    • Open-source BI: Superset/Metabase
    • Loginom
    • Yandex.DataLens
    • AI / Исскуственный интеллект
    • Optimacros
    • Шины данных
  • Курсы
    • Учебный курс Информационная грамотность
    • Учебный курс для бизнес-аналитиков
    • Учебный курс для системных аналитиков
    • Учебный курс по Data Governance
    • Учебный курс Как стать CDO
    • Учебный курс Современная архитектура хранилища данных
    • Учебный курс по Fine BI
    • Учебный курс по FineReport
    • Учебный курс по DWH
    • Учебный курс по Data Science (ML, AI)
    • Учебный курс по PostgreSQL
    • Учебный курс по Apache Airflow и NiFi
    • Учебный курс по Open-source BI
    • Учебный курс по ClickHouse
    • Учебный курс по DataLens
    • Учебный курс по Loginom
    • Учебный курс по Modus BI и ETL
    • Учебный курс по Visiology
    • Учебный курс по dbt
  • Функциональные решения
    • Создание Data Lake
    • Цифровая трансформация
    • Управление по KPI
    • Финансы
    • Продажи
    • Склад
    • HR
    • Маркетинг
    • Внутренний аудит
    • Категорийный менеджмент
    • S&OP и прогнозная аналитика
    • Геоаналитика
    • Цепочки поставок (SCM)
    • AutoML
    • Process Mining
    • Сквозная аналитика
  • Компания
    • О нас
    • Руководство
    • Новости
    • Клиенты
    • Скачать
    • Контакты
    • Политика конфиденциальности
RutubeVkontakteLinkedInYouTube
ООО "Би Ай Консалт",
ИНН: 7811437757,
ОГРН: 1097847154184
199178, Россия,
Санкт-Петербург,
6-ая линия В.О., Д. 63, 4 этаж
Тел: +7 (812) 334-08-01
Тел: +7 (499) 608-13-06
E-mail: info@biconsult.ru

 

 

 

 

 

×

Пользуясь сайтом, вы соглашаетесь с использованием cookies и политикой конфиденциальности.