Павло Щербуха

Logo

Персональна освітня сорінка

Розробка на Node.js, VUE.js, Python, IBM Integration Bus (App Connect Ent) , ORACLE PL/SQL
2 March 2023

Keycloak vue.js node.js on openhsift

by Pavlo Shcherbukha

Інтеграція keycloak з vue.js, Node.js express в openhsift

Продук Keycloack є зараз типовим інструментом для авторизації в Web-based системах. Документацію можна знайти за лінками:

Ну на цьому зацікавився цим продуктом, щоб трохи розібратися як він працює та як його конфігурвати. Це більше про те, як підключитися клієнтом до keycloak а не про те, як правильно його розгортати та конфігурувати.

Розгортаня keycloak в OpenShift

За звичай keycloak можна підняти в контейнері, але він страртує в development mode. Також, там треба прив’язати базу даних типу postgresql. Я вирішив піти більш простим шляхом і розгорнув його в хмарі RadHat в sendbox OpsnShift. Openshift sendbox можна створити за url: https://developers.redhat.com/developer-sandbox, а перед цим потрібно зарєструватися як developer на RedHat. Docker відкинув зразу, тому що прочитав ліцензійні обмеження для корпорацій, що описані за лінокм pricing в самому низу стріник, і відмовився.

Docker Desktop is free to use, as part of the Docker Personal subscription, for individuals, non-commercial open source developers, students and educators, and small businesses of less than 250 employees AND less than $10 million in revenue. Commercial use of Docker Desktop at a company of more than 250 employees OR more than $10 million in annual revenue requires a paid subscription (Pro, Team, or Business) to use Docker Desktop. While the effective date of these terms is August 31, 2021, there is a grace period until January 31, 2022 for those that require a paid subscription to use Docker Desktop.

Щоб не мучитися з лінкуванням бази даних та самого сервісу keycloak я використав калог уже підготованих продуктів, що є вже в любому OpenShift і в пару кліків розгорнув додаток pic-01.

pic-01

Через кілька хвилин я уже маю готовий keycloak:

pic-02

І уже натискаємо на роут pic-03, попадаємо в консоль адміністрування. Можливо потрібно трохи зачекати, поки обидва сервіса стартонуть. Ну, там час старту такий собі, відчутний, але не критично.

pic-03

Консоль адміністрування запросить логін та пароль. Вони знаходяться в env змінних Keycloak так як показано на pic-04

pic-04

Залогінился і вуаля - попадаємо в консоль адмінітрування, в головний realm

pic-05

Основні терміни адміністрування Keycloak

Адміністрування в keycloak ділиться на realm-и.

pic-06

Keycloak підтримує як OpenID Connect (розширення OAuth 2.0), так і SAML 2.0. Коли говоримо про security, перше, що потрібно вирішити, це те, що з двох ви збираєтеся використовувати. Я вирішую використовувати OpenID Connect (розширення OAuth 2.0), тому в подальшому мова йде тільки про нього.

Ручна реєстрація програми клієнта

Створюємо клієнта в client ID DemoApp1, як показано на pic-06:

pic-06

Після реєстрації зразу попадаємо на вікно pic-07

pic-07

Тут треба звернути увагу на Access Type=public при логіні не поребує client Secret. А при Access Type=confidential потребує client Secret. Зараз залишаємо public.

Треба звернути увагу на Standard Flow Enabled треба включити в “ON”. Таким чином авторизація піде по стандартному OAuth2.0 за допомогою обміну autorization_code.

Далі потрібно налаштувати URL додатка так, як показано на pic-08.

pic-08

Тобто, поки що нас цікавлять Root URL та “правильний” redirect URL. По ньому відбувається переадресація у випадку успішного логіна. Інші, пока що не задіяні.

Все, можна сказати в найпростішому варіанті клієнт-додаток зареєстровано.

Налаштування прикладних ролей для програми клієнта

Після реєстрації додатку-клієнта потрібно запараметризувати рольовий доступ до ресурсів програми. Рольовий досутп може ділитися на по ролям. А ролі діляться на Realm Roles та Client Roles.

Для нашого майбутньго додатку створемо 2 ролі:

Для цього створюємо ролі, як показано на pic-09

pic-09

Щоб ролі передавалися на клієнта, потрібно пересідчитися, що вони включені в client scope, як на pic-10.

pic-10

Створення користувачів для призначення їм ролей

Набір коритсувачів є єдиним на весь realm. Але користувачів можна розділити на групи. Набір груп теж єедний на весь realm. А от групи можна вже поэднати з ролями. Так і зробимо.

Процес створення користувачыв показано на pic-11.

pic-11

Тепер створимо групи користовучів і поділимо користувачів на дві групи:

Процес створення груп показано на pic-12 та pic-13.

pic-12

pic-13

Далі, потрібно пройтися по користувачах і додати кожного у відповідін групи так, як показано на pic-14.

pic-14

Тут треба зазначити, що якщо keycloak інтегрувати з Active Directory - то групи користувачів будть зразу відображатися і розносити користувачів по групах в KeyCloak не потрібно. Це робиться в Active Directory. У підсумку, користувачі по групах рознесені так, як показано на pic-15.

pic-15

На цьому етапі співставлення: користувачі-групи-client roles виконано. можна переходити до етапа тестування логіну та отримання авторизаційногг токену.

Отримання авторизаційного токену по протоколу openid-connect

Для перевірки авторизації потрібно визначити, для початку, знайти “правильні” URL. Для цього. ідемо в налаштування realm та отримуємо json, як показано на pic-16.

pic-16

Далі беремо url в ключі token_endpoint та формуємо http - запит на KeyCloack


    Content-Type: application/x-www-form-urlencoded

    grant_type=password&client_id=DemoApp1&username=usr1&password=11111111

{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJaUUhtZ0oya2R1b1FPOG12NTVoYU1vOXJKd1hMYVBGZ3VJRU9JejA5Y0xvIn0.eyJleHAiOjE2NzgyODAxODgsImlhdCI6MTY3ODI3OTg4OCwianRpIjoiMjE3NGNhNmUtZjcxMC00MDFkLWJiOGMtYzBjOTBlZWI0N2VhIiwiaXNzIjoiaHR0cHM6Ly9zc28tcGFzaGFreC1kZXYuYXBwcy5zYW5kYm94LW0zLjE1MzAucDEub3BlbnNoaWZ0YXBwcy5jb20vYXV0aC9yZWFsbXMvc2hkZW1vcmVhbG0iLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiNGJiNTQzZDktOGQ4NS00ZTcxLTlkOTYtMTA1YWNiNTE2MjNlIiwidHlwIjoiQmVhcmVyIiwiYXpwIjoiRGVtb0FwcDEiLCJzZXNzaW9uX3N0YXRlIjoiMzBhN2YwODMtNGFjZi00ZWY2LTg0MGEtZDA4YTYzZmMyNzAyIiwiYWNyIjoiMSIsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJkZWZhdWx0LXJvbGVzLXNoZGVtb3JlYWxtIiwib2ZmbGluZV9hY2Nlc3MiLCJ1bWFfYXV0aG9yaXphdGlvbiJdfSwicmVzb3VyY2VfYWNjZXNzIjp7IkRlbW9BcHAxIjp7InJvbGVzIjpbImFwcF92aWV3ZXIiXX0sImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoicHJvZmlsZSBlbWFpbCIsInNpZCI6IjMwYTdmMDgzLTRhY2YtNGVmNi04NDBhLWQwOGE2M2ZjMjcwMiIsImVtYWlsX3ZlcmlmaWVkIjpmYWxzZSwibmFtZSI6ItCf0LXRgtGA0L4g0J_QtdGC0YDQtdC90LrQviIsInByZWZlcnJlZF91c2VybmFtZSI6InVzcjEiLCJnaXZlbl9uYW1lIjoi0J_QtdGC0YDQviIsImZhbWlseV9uYW1lIjoi0J_QtdGC0YDQtdC90LrQviJ9.S-xsR0RTac3YwF7hVX5f3N1W4qkoXcIXnnfIQVMKRAi99LCt4UqEKGIGBf_QtbGikMLnm7sSHJAesJIOoVURYklVNM0Hxb6xgy4gy6KGcgxabbeuDdLJsN2OVENTtFL9m3GQAAlN71w7ka8MmMxcxqYhEAxQtVwvCU1pa6tpkOpoQKpRB9CeNRuhx9KdziymvNR4AEhD8Q1QqB0yLvyjxQ_pkKGT808IpWPezvqMGXuLQ2yEfDV8sc3PFyQMhDCwLj5W-egvyJJq2bIgssNNYJ69WsX_WcnfPWOisErGIMijcNq7rFCEwrmIUdrTaTxyEfeJfJgU0MjJd1a-440WIA",
  "expires_in": 300,
  "refresh_expires_in": 1800,
  "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJkYTVmZWIwOS1hNWYxLTQwZjMtYjc5MC1iYTI0NGJkYTIyMDIifQ.eyJleHAiOjE2NzgyODE2ODgsImlhdCI6MTY3ODI3OTg4OCwianRpIjoiNjk0MTRkMGYtMTAyZS00Yzc1LThlNmQtNjRmZmY1ZjhmODhjIiwiaXNzIjoiaHR0cHM6Ly9zc28tcGFzaGFreC1kZXYuYXBwcy5zYW5kYm94LW0zLjE1MzAucDEub3BlbnNoaWZ0YXBwcy5jb20vYXV0aC9yZWFsbXMvc2hkZW1vcmVhbG0iLCJhdWQiOiJodHRwczovL3Nzby1wYXNoYWt4LWRldi5hcHBzLnNhbmRib3gtbTMuMTUzMC5wMS5vcGVuc2hpZnRhcHBzLmNvbS9hdXRoL3JlYWxtcy9zaGRlbW9yZWFsbSIsInN1YiI6IjRiYjU0M2Q5LThkODUtNGU3MS05ZDk2LTEwNWFjYjUxNjIzZSIsInR5cCI6IlJlZnJlc2giLCJhenAiOiJEZW1vQXBwMSIsInNlc3Npb25fc3RhdGUiOiIzMGE3ZjA4My00YWNmLTRlZjYtODQwYS1kMDhhNjNmYzI3MDIiLCJzY29wZSI6InByb2ZpbGUgZW1haWwiLCJzaWQiOiIzMGE3ZjA4My00YWNmLTRlZjYtODQwYS1kMDhhNjNmYzI3MDIifQ.m6MbJ1bVlbixDzURut5vOPmWVS5aD2o2GCw3v20YOY0",
  "token_type": "Bearer",
  "not-before-policy": 0,
  "session_state": "30a7f083-4acf-4ef6-840a-d08a63fc2702",
  "scope": "profile email"
}

Ну, або у вигляді curl

curl -X POST -k -H 'Content-Type: application/x-www-form-urlencoded' -i 'https://[domain.name]/auth/realms/shdemorealm/protocol/openid-connect/token' --data 'grant_type=password&client_id=DemoApp1&username=usr1&password=11111111'

Ну а якщо зайти в меню sessions або users, то можна побачити всі підключені сесії жо жаного клієнта, та є можливісь всіх, або окремо взятого користувача - відключити pic-17.

pic-17

Під одним логіном можна отримати кілька токенів і вони всі будуть дійсні, якщо їх період дії пересікається. Ну можна вибрати ще одного користувача, usr4 pic-18.

pic-18

Захист Node.js express Rest API за допомогою KeyCloak

В документації до Keycloak Securing Applications and Services Guide є розділ по інтеграції додатків Node.js з keycloak 2.3. Node.js adapter. Рекомендується використовувати пакет keycloak-connect, при цьому він парцює разом з express-session.

Тому для демонстрації підготовано приклад простого додатку todo_srvc - Node.js exptress REST API with keycloak security, що використовує keycloak-connect для захисту RestAPI. Опист розгортання додтаку та розробені API описано в readme.md. Додаток використовує realm та client_id, що були створені в попередньому розділі.

Завантаження бібліотеки keycloak-connect відубвається у файлі /server/config/keycloak-config.js. А вже саме її підклчення для викоритсання, разом з express-session, виконується в файлі /server/server.js:

// Включаємо  session memory store
const memoryStore = new session.MemoryStore()

app.use(session({
  secret: 'mySecret',
  resave: false,
  saveUninitialized: true,
  store: memoryStore
}))


// включаємо keycloack
const keycloak = require('./config/keycloak-config.js').initKeycloak(memoryStore);
app.use(keycloak.middleware());

Також, потрібно звернути увагу на те, що всі АПІ, які не повинні використовувати захист keycloak потрібно розмістити вище цього участку коду. Інакше, keycloak буде перевіряти наявність http-заголовка Authorization з токеном:

  Authorization: Bearer <token>

А, для прикладу, коли у вас використовується fronend, то браузер шле на backend http запити options і явно без цього заголовка. Тому, в цій демці методи options та метод перевірки доступності сервера “api/health” розміщені до момента підключення keycloak.

Як було показано раніше, на рівні клієнта створено 2 ролі: app_viewer та app_editor. Для захисту API можна використати конструкцію keycloak.protect, де в масиві передати список доступних ролей.

keycloak.protect(  [ 'app_editor' ,'app_viewe' ]  )

Ось для прикладу:

app.get('/api/todos',  keycloak.protect(  [ 'app_editor' ,'app_viewe' ]  ), function(req, res) {
  let label='todos';
  applog.info( 'call api/todos method', label);
  try{
      let result=i_todos;
      return res.status(200).json( result );
  } 
  catch (err){
      applog.error( `Error ${err.message} `, label);
      errresp=applib.HttpErrorResponse(err)
      applog.error( `Error result ${errresp.Error.statusCode} ` + JSON.stringify( errresp )   ,label);
      return res.status(errresp.Error.statusCode ).json(errresp);    

  }

});  

app.post('/api/todo',  keycloak.protect( [ 'app_editor' ]),  function(req, res) {
  let label='todo';
  applog.info( 'call api/todo method', label);
  let body=req.body;
  try { 
      applog.info( 'Check propery [name]', label);
      if (!body.hasOwnProperty("name")){
        throw new apperror.ValidationError( 'key [name] is absend' );
      }
      applog.info( 'Check propery [description]', label);
      if (!body.hasOwnProperty("description")){
        throw new apperror.ValidationError( 'key [description] is absend' );

      }
      applog.info( 'Check propery [owner]', label);
      if (!body.hasOwnProperty("owner")){
        throw new apperror.ValidationError( 'key [owner] is absend' );
      }
      applog.info( 'Return result', label);
      
      body["id"] = uuid.v4()
      i_todos.push(  body )
      let result={"id": body.id};
      return res.status(200).json( result );

  } 
  catch (err){
    applog.error( `Error ${err.message} `, label);
    errresp=applib.HttpErrorResponse(err)
    applog.error( `Error result ${errresp.Error.statusCode} ` + JSON.stringify( errresp )   ,label);
    return res.status(errresp.Error.statusCode ).json(errresp);

  }

});

Але, як на мене, це не дуже гнучний метод. Мені б хотілося, з токена отримати інформацію про користувача, та номер сесії. Більш того, хотілося б якось запараметризувати відповідність URL (path) та доступних ролей. Цього можна досягти, якщо прочитиати увжано документацію до бібліотеки, де сказано, що в keycloak.protect() можна використовувати не тільки масив ролей а і свою функцію, яка в якості парамтерів приймає token та requset, так, як показано далі фрагмент функції та приклад її використання. Перевірка наявності тієї чи іншої ролі виконується функцією token.hasRole( “rolename”). А сама функція контролю доступа повинна повертати boolean: true-дозволено, false-не дозволено.

/**
 * Перевірка доступа
 * @param {*} token 
 * @param {*} request 
 * @returns  true or false
 */
function checkAccess(token, request) {
  let label='checkAccess';
  let is_role=false ;
  if ( token.hasRole( "rolename") ) {
      is_role=true ;

  }

  applog.info(`checkAccess Method: ${request.method} Path: ${request.path}`, label);
  return is_role;
}  


app.post('/api/todo',   keycloak.protect(  checkAccess  ) ,  function(req, res) {
  let label='todo';
  applog.info( 'call api/todo method', label);
});  


От цей підхід і можна використати. Для цього потібно підготувати json стурктуру, що пов’язує:

На приклад, зробимо такий /server/config/accessRoles.json :

[
  {"method": "GET",    "path": "/api/todos", "accessroles": ["app_editor", "app_viewer" ]},
  {"method": "POST",   "path": "/api/todo", "accessroles": ["app_editor"]},
  {"method": "GET",    "path": "/api/todo/:todoid", "accessroles": ["app_viewer", "app_editor" ]},
  {"method": "DELETE", "path": "/api/todo/:todoid", "accessroles": ["app_editor"]}
]

Для співставлення path реального URL та заданого в файлі використаємо пакет: url-pattern. Доречі, виявив, що пакет на працює з масками імен, що майть “_”: “:todoid - співставляє”, а “:todo_id - не співставляє”. Фінальний варіант функції та її використання показані уже в git репозиторії. На додачу до цього додамо логування в метода реквізити username та state (що аналогічно ідентифікатору сесії).

Також, слід звернути увагу на частину куду в функції checkAccess, а саме:

{
  token: "eyJhbGc.......jEmYLHmeg",
  clientId: "DemoApp1",
  header: {
    alg: "RS256",
    typ: "JWT",
    kid: "jott31qT3_Vutuz6yt9k2......lsJizig",
  },
  content: {
    exp: 1678552102,
    iat: 1678551802,
    jti: "21954b73-6579-4e42-b260-8f08840b0084",
    iss: "https://hostname/auth/realms/DemoApp1",
    aud: "account",
    sub: "228501ff-8d5c-4224-9fe7-8a72fa5db426",
    typ: "Bearer",
    azp: "DemoApp1",
    session_state: "9310df83-1f52-43b1-b64f-6cc74d090c8f",
    acr: "1",
    "allowed-origins": [
      "",
    ],
    realm_access: {
      roles: [
        "offline_access",
        "uma_authorization",
        "default-roles-iitregsrvcr",
      ],
    },
    resource_access: {
      DemoApp1: {
        roles: [
          "app_editor",
        ],
      },
      account: {
        roles: [
          "manage-account",
          "manage-account-links",
          "view-profile",
        ],
      },
    },
    scope: "email profile",
    sid: "9310df83-1f52-43b1-b64f-6cc74d090c8f",
    email_verified: false,
    name: "Микола Роблюусьо",
    preferred_username: "usr1",
    given_name: "Микола",
    family_name: "Роблюусьо",
  },
  signature: new Uint8Array([53, .........2]),
  signed: "eyJ.....QviJ9",
}


function checkAccess(token, request) {
  let label='checkAccess';
  let is_role=false ;
  applog.info(`checkAccess Method: ${request.method} Path: ${request.path}`, label);
  // в параметри http сесії записуємо token-об'єкт
  applog.info("Set session param ", label);
  request.session["keycloak-token"]=token;
  ///..........
}

app.get('/api/todos',  keycloak.protect( checkAccess ), function(req, res) {
  let label='todos';
  // отримуэмо з запиту об'єкт-token  
  let ssnk=req.session['keycloak-token'];

  let alogctx= new LogContext();
  let alog= new AppLogger();
  alog.LogContext=alogctx;      
  
  // з token  читаю id сесії та логін користувача і заисую в лог для подальшого використання
  alog.State=ssnk.content.session_state;  
  alog.Username=ssnk.content.preferred_username;
  
  
  alog.info( 'call api/todos method', label);
  // ...........
});


[
  /* роль перевіряються в client-id  backend*/
  {"method": "GET",    "path": "/api/todos", "accessroles": ["app_editor", "app_viewer" ]},
  /*перевіряється роль для client-id "demoapp2"*/
  {"method": "POST",   "path": "/api/todo", "accessroles": ["demoapp2:app_editor"]},
  {"method": "GET",    "path": "/api/todo/:todoid", "accessroles": ["demoapp2:app_viewer", "demoapp2:app_editor" ]},
  /*В цьому прикладіперевіряється роль, що задана на весь realm*/
  {"method": "DELETE", "path": "/api/todo/:todoid", "accessroles": ["realm:app_support"]}
]

Таким чином, один back-end може перевіряти доступи кількох front-end в рамках одного realm. А, для приклду, групі support можна зробити розширені доступа до всіх методів за допомогою введення одної спільної ralm- ролі.

Ось лог роботи сервісу, коли методи виклкаємо для користуачів usr1 - app_viewer та usr4 - app_editor. По логу можна побачити, що метод POST Path: /api/todo (label=createTodo) достпно для користувача usr4 та не доступно для usr1, чого і хотіли досягти. И бачимо, що викли кожного метода супрводжується логуванням, в якому присутні username - логін користуача та state-ідентифікатор сесії. Тобто по ньому можна відслідкувати послідовність викликів сесії.

        [
            {
                "hostname": "localhost",
                "label": "server",
                "level": "info",
                "message": "SERVER HAS STARTED",
                "state": null,
                "timestamp": "2023-03-13T21:23:13.765765+02:00",
                "username": null
            },
            {
                "hostname": "localhost",
                "label": "server",
                "level": "info",
                "message": "LISTENING  PORT= 8080 on HOST localhost",
                "state": null,
                "timestamp": "2023-03-13T21:23:13.767767+02:00",
                "username": null
            },
            {
                "hostname": "localhost",
                "label": "checkAccess",
                "level": "info",
                "message": "checkAccess Method: GET Path: /api/todos",
                "state": null,
                "timestamp": "2023-03-13T21:24:01.980980+02:00",
                "username": null
            },
            {
                "hostname": "localhost",
                "label": "checkAccess",
                "level": "info",
                "message": "Set session param ",
                "state": null,
                "timestamp": "2023-03-13T21:24:01.983983+02:00",
                "username": null
            },
            {
                "hostname": "localhost",
                "label": "checkAccess",
                "level": "info",
                "message": "Check parmission of: usr1 - Петро Петренко state= a9552b74-24ac-4926-87ec-1ccc4d6b026a",
                "state": null,
                "timestamp": "2023-03-13T21:24:01.985985+02:00",
                "username": null
            },
            {
                "hostname": "localhost",
                "label": "checkAccess",
                "level": "info",
                "message": "Check parmission : usr1 - Петро Петренко state= a9552b74-24ac-4926-87ec-1ccc4d6b026a  against Method: GET Path: /api/todos  RESULT: true",
                "state": null,
                "timestamp": "2023-03-13T21:24:01.993993+02:00",
                "username": null
            },
            {
                "hostname": "localhost",
                "label": "todos",
                "level": "info",
                "message": "call api/todos method",
                "state": "a9552b74-24ac-4926-87ec-1ccc4d6b026a",
                "timestamp": "2023-03-13T21:24:01.997997+02:00",
                "username": "usr1"
            },
            {
                "hostname": "localhost",
                "label": "checkAccess",
                "level": "info",
                "message": "checkAccess Method: POST Path: /api/todo",
                "state": null,
                "timestamp": "2023-03-13T21:24:21.643643+02:00",
                "username": null
            },
            {
                "hostname": "localhost",
                "label": "checkAccess",
                "level": "info",
                "message": "Set session param ",
                "state": null,
                "timestamp": "2023-03-13T21:24:21.645645+02:00",
                "username": null
            },
            {
                "hostname": "localhost",
                "label": "checkAccess",
                "level": "info",
                "message": "Check parmission of: usr1 - Петро Петренко state= a9552b74-24ac-4926-87ec-1ccc4d6b026a",
                "state": null,
                "timestamp": "2023-03-13T21:24:21.647647+02:00",
                "username": null
            },
            {
                "hostname": "localhost",
                "label": "checkAccess",
                "level": "info",
                "message": "Check parmission : usr1 - Петро Петренко state= a9552b74-24ac-4926-87ec-1ccc4d6b026a  against Method: POST Path: /api/todo  RESULT: false",
                "state": null,
                "timestamp": "2023-03-13T21:24:21.651651+02:00",
                "username": null
            },
            {
                "hostname": "localhost",
                "label": "checkAccess",
                "level": "info",
                "message": "checkAccess Method: GET Path: /api/todos",
                "state": null,
                "timestamp": "2023-03-13T21:24:52.391391+02:00",
                "username": null
            },
            {
                "hostname": "localhost",
                "label": "checkAccess",
                "level": "info",
                "message": "Set session param ",
                "state": null,
                "timestamp": "2023-03-13T21:24:52.394394+02:00",
                "username": null
            },
            {
                "hostname": "localhost",
                "label": "checkAccess",
                "level": "info",
                "message": "Check parmission of: usr4 - Денис Денисов state= c41a18f6-7617-4415-a477-86c5950f691c",
                "state": null,
                "timestamp": "2023-03-13T21:24:52.395395+02:00",
                "username": null
            },
            {
                "hostname": "localhost",
                "label": "checkAccess",
                "level": "info",
                "message": "Check parmission : usr4 - Денис Денисов state= c41a18f6-7617-4415-a477-86c5950f691c  against Method: GET Path: /api/todos  RESULT: true",
                "state": null,
                "timestamp": "2023-03-13T21:24:52.399399+02:00",
                "username": null
            },
            {
                "hostname": "localhost",
                "label": "todos",
                "level": "info",
                "message": "call api/todos method",
                "state": "c41a18f6-7617-4415-a477-86c5950f691c",
                "timestamp": "2023-03-13T21:24:52.403403+02:00",
                "username": "usr4"
            },
            {
                "hostname": "localhost",
                "label": "checkAccess",
                "level": "info",
                "message": "checkAccess Method: POST Path: /api/todo",
                "state": null,
                "timestamp": "2023-03-13T21:25:06.717717+02:00",
                "username": null
            },
            {
                "hostname": "localhost",
                "label": "checkAccess",
                "level": "info",
                "message": "Set session param ",
                "state": null,
                "timestamp": "2023-03-13T21:25:06.720720+02:00",
                "username": null
            },
            {
                "hostname": "localhost",
                "label": "checkAccess",
                "level": "info",
                "message": "Check parmission of: usr4 - Денис Денисов state= c41a18f6-7617-4415-a477-86c5950f691c",
                "state": null,
                "timestamp": "2023-03-13T21:25:06.721721+02:00",
                "username": null
            },
            {
                "hostname": "localhost",
                "label": "checkAccess",
                "level": "info",
                "message": "Check parmission : usr4 - Денис Денисов state= c41a18f6-7617-4415-a477-86c5950f691c  against Method: POST Path: /api/todo  RESULT: true",
                "state": null,
                "timestamp": "2023-03-13T21:25:06.725725+02:00",
                "username": null
            },
            {
                "hostname": "localhost",
                "label": "createtodo",
                "level": "info",
                "message": "call post api/todo method",
                "state": "c41a18f6-7617-4415-a477-86c5950f691c",
                "timestamp": "2023-03-13T21:25:06.729729+02:00",
                "username": "usr4"
            },
            {
                "hostname": "localhost",
                "label": "createtodo",
                "level": "info",
                "message": "Check propery [name]",
                "state": "c41a18f6-7617-4415-a477-86c5950f691c",
                "timestamp": "2023-03-13T21:25:06.732732+02:00",
                "username": "usr4"
            },
            {
                "hostname": "localhost",
                "label": "createtodo",
                "level": "info",
                "message": "Check propery [description]",
                "state": "c41a18f6-7617-4415-a477-86c5950f691c",
                "timestamp": "2023-03-13T21:25:06.734734+02:00",
                "username": "usr4"
            },
            {
                "hostname": "localhost",
                "label": "createtodo",
                "level": "info",
                "message": "Check propery [owner]",
                "state": "c41a18f6-7617-4415-a477-86c5950f691c",
                "timestamp": "2023-03-13T21:25:06.736736+02:00",
                "username": "usr4"
            },
            {
                "hostname": "localhost",
                "label": "createtodo",
                "level": "info",
                "message": "Return result",
                "state": "c41a18f6-7617-4415-a477-86c5950f691c",
                "timestamp": "2023-03-13T21:25:06.738738+02:00",
                "username": "usr4"
            }
        ]
tags: