From 7565b6344d1392dc1a3da8a5a4a5a8bde262cc1b Mon Sep 17 00:00:00 2001 From: Maxime Lagresle Date: Wed, 2 Oct 2024 09:48:16 +0200 Subject: [PATCH] more tests for login with API key --- .../embedded/fixtures/create_accounts_test.go | 10 +++ .../fixtures/test-kdf0_GET_api_sync.json | 74 +++++++++++++++++++ .../test-kdf0_POST_api_accounts_api-key.json | 5 ++ .../fixtures/test-kdf0_POST_api_ciphers.json | 16 ++-- ...test-kdf0_POST_identity_connect_token.json | 13 +--- .../fixtures/test-kdf1_GET_api_sync.json | 74 +++++++++++++++++++ .../test-kdf1_POST_api_accounts_api-key.json | 5 ++ .../fixtures/test-kdf1_POST_api_ciphers.json | 16 ++-- ...test-kdf1_POST_identity_connect_token.json | 13 +--- internal/bitwarden/embedded/vault_webapi.go | 15 ++++ .../bitwarden/embedded/vault_webapi_test.go | 36 +++++++++ internal/bitwarden/models/models.go | 6 ++ internal/bitwarden/webapi/client.go | 21 ++++++ internal/bitwarden/webapi/models.go | 6 ++ 14 files changed, 272 insertions(+), 38 deletions(-) create mode 100644 internal/bitwarden/embedded/fixtures/test-kdf0_POST_api_accounts_api-key.json create mode 100644 internal/bitwarden/embedded/fixtures/test-kdf1_POST_api_accounts_api-key.json diff --git a/internal/bitwarden/embedded/fixtures/create_accounts_test.go b/internal/bitwarden/embedded/fixtures/create_accounts_test.go index 899ce0b..8b99a10 100644 --- a/internal/bitwarden/embedded/fixtures/create_accounts_test.go +++ b/internal/bitwarden/embedded/fixtures/create_accounts_test.go @@ -101,6 +101,16 @@ func createTestAccount(t *testing.T, accountEmail string, kdfConfig models.KdfCo t.Fatal(err) } + apiKey, err := vault.GetAPIKey(ctx, accountEmail, TestPassword) + if err != nil { + t.Fatal(err) + } + + err = vault.LoginWithAPIKey(ctx, TestPassword, apiKey.ClientID, apiKey.ClientSecret) + if err != nil { + t.Fatal(err) + } + _, err = vault.CreateObject(ctx, models.Object{ Object: models.ObjectTypeItem, Type: models.ItemTypeLogin, diff --git a/internal/bitwarden/embedded/fixtures/test-kdf0_GET_api_sync.json b/internal/bitwarden/embedded/fixtures/test-kdf0_GET_api_sync.json index d6e2dd3..4350e21 100644 --- a/internal/bitwarden/embedded/fixtures/test-kdf0_GET_api_sync.json +++ b/internal/bitwarden/embedded/fixtures/test-kdf0_GET_api_sync.json @@ -1,5 +1,42 @@ { "ciphers": [ + { + "attachments": null, + "card": null, + "collectionIds": [], + "creationDate": "2024-10-02T07:42:36.462697Z", + "data": { + "fields": [], + "name": "2.JtYOMVeWZcP4enXUHfkAng==|F6nhgNFcKmIb//4YaFYqgnnJ0WZ078BHTkSUSGKMytY=|9gS6j3xzbIL3iBkPeA3fA3WaqcM7kmRE9LTXGFudxEc=", + "notes": null, + "passwordHistory": [], + "uri": null, + "username": "2.Qx257ROoxcl4g410tpimeA==|lCmrwQQYF43LtGa7f1Gr0Q==|kkco4xdNJWRDWBJ1pDsnkyJzpAFi9EBf+VunlVenwiE=" + }, + "deletedDate": null, + "edit": true, + "favorite": false, + "fields": [], + "folderId": null, + "id": "42ae27ca-64a6-4ddc-a488-1436bfc4883e", + "identity": null, + "key": "2.RyTOBsgtSyL0Omu4XnUakQ==|bF+NAs2MCuN1sEye0YR3uQqrgncZxNxrYODlEUL7W7XXbFxKaGVo0fpevMFo8Ov/c1Wb8KfIWyGHC1T/cezG0GMONMeyMreaUl3LbWmRKtA=|3BiW9s0qPt3bgPu7ok78hzPC8Gf9A2/rL/oU5KEODdw=", + "login": { + "uri": null, + "username": "2.Qx257ROoxcl4g410tpimeA==|lCmrwQQYF43LtGa7f1Gr0Q==|kkco4xdNJWRDWBJ1pDsnkyJzpAFi9EBf+VunlVenwiE=" + }, + "name": "2.JtYOMVeWZcP4enXUHfkAng==|F6nhgNFcKmIb//4YaFYqgnnJ0WZ078BHTkSUSGKMytY=|9gS6j3xzbIL3iBkPeA3fA3WaqcM7kmRE9LTXGFudxEc=", + "notes": null, + "object": "cipherDetails", + "organizationId": null, + "organizationUseTotp": true, + "passwordHistory": [], + "reprompt": 0, + "revisionDate": "2024-10-02T07:42:36.462837Z", + "secureNote": null, + "type": 1, + "viewPassword": true + }, { "attachments": null, "card": null, @@ -35,6 +72,43 @@ "type": 1, "viewPassword": true }, + { + "attachments": null, + "card": null, + "collectionIds": [], + "creationDate": "2024-10-02T07:42:54.189187Z", + "data": { + "fields": [], + "name": "2.w4kzDaZLWBQLwFMffQUc4Q==|gwtAiY/86hyH/kh/diHlJBQ6wlQZc4xMrpHvkVzdGp8=|gqF2h3FN9S5hiDK1kASO8wBjNp8ognh8w25tsKfA+Go=", + "notes": null, + "passwordHistory": [], + "uri": null, + "username": "2./SO1TFQJ9BxFipSJIuhVeg==|VapfwOlRcxVnNj+5Tz8YLg==|0MJNPVx43qXBou7o84jccWRumDG+GQqkxexbhArGUFc=" + }, + "deletedDate": null, + "edit": true, + "favorite": false, + "fields": [], + "folderId": null, + "id": "dced7812-e7a3-4163-b50c-74bee6126553", + "identity": null, + "key": "2.yoo9dZOltLBIDjLudHfx+A==|jNRvIbWF+7FjAq0DcB00oYiGOSKH9DdRk4y1pbLxvZzTaAL637831eiru9Prq6jUdjDdryvqxQFAOm8sRO40s+VVUBdI48ILc/WO+FUT7do=|QAgU5av2dp8zRmbWdxbullH//bo0IdNS8u3JAG+/eJE=", + "login": { + "uri": null, + "username": "2./SO1TFQJ9BxFipSJIuhVeg==|VapfwOlRcxVnNj+5Tz8YLg==|0MJNPVx43qXBou7o84jccWRumDG+GQqkxexbhArGUFc=" + }, + "name": "2.w4kzDaZLWBQLwFMffQUc4Q==|gwtAiY/86hyH/kh/diHlJBQ6wlQZc4xMrpHvkVzdGp8=|gqF2h3FN9S5hiDK1kASO8wBjNp8ognh8w25tsKfA+Go=", + "notes": null, + "object": "cipherDetails", + "organizationId": null, + "organizationUseTotp": true, + "passwordHistory": [], + "reprompt": 0, + "revisionDate": "2024-10-02T07:42:54.189295Z", + "secureNote": null, + "type": 1, + "viewPassword": true + }, { "attachments": null, "card": null, diff --git a/internal/bitwarden/embedded/fixtures/test-kdf0_POST_api_accounts_api-key.json b/internal/bitwarden/embedded/fixtures/test-kdf0_POST_api_accounts_api-key.json new file mode 100644 index 0000000..36c8437 --- /dev/null +++ b/internal/bitwarden/embedded/fixtures/test-kdf0_POST_api_accounts_api-key.json @@ -0,0 +1,5 @@ +{ + "apiKey": "ZTXHHyPY6bNlNq1diDA2nM1GROboP3", + "object": "apiKey", + "revisionDate": "2024-10-02T07:42:36.462941Z" +} \ No newline at end of file diff --git a/internal/bitwarden/embedded/fixtures/test-kdf0_POST_api_ciphers.json b/internal/bitwarden/embedded/fixtures/test-kdf0_POST_api_ciphers.json index 384747c..c499f16 100644 --- a/internal/bitwarden/embedded/fixtures/test-kdf0_POST_api_ciphers.json +++ b/internal/bitwarden/embedded/fixtures/test-kdf0_POST_api_ciphers.json @@ -2,35 +2,35 @@ "attachments": null, "card": null, "collectionIds": [], - "creationDate": "2024-10-02T07:23:54.495716Z", + "creationDate": "2024-10-02T07:42:54.189187Z", "data": { "fields": [], - "name": "2.1r73Ar0+UEKhQ6/HJlgy6w==|JJ9AyivpozzvKk5fdyHirGbsDQI1kwlVr6xZoAZt6dg=|5Xy/XeqN6YdLKC4dVCECydxJbBTonMq9tq6e0/Y0bGw=", + "name": "2.w4kzDaZLWBQLwFMffQUc4Q==|gwtAiY/86hyH/kh/diHlJBQ6wlQZc4xMrpHvkVzdGp8=|gqF2h3FN9S5hiDK1kASO8wBjNp8ognh8w25tsKfA+Go=", "notes": null, "passwordHistory": [], "uri": null, - "username": "2.2slHKGwhT9pYZNuZJZELtQ==|2S8vFbqTdE2O5jghb5fIPg==|wUYsi6TFyOdFL1nJG16nO3FMRH4QAtVyNb1vzchcIg8=" + "username": "2./SO1TFQJ9BxFipSJIuhVeg==|VapfwOlRcxVnNj+5Tz8YLg==|0MJNPVx43qXBou7o84jccWRumDG+GQqkxexbhArGUFc=" }, "deletedDate": null, "edit": true, "favorite": false, "fields": [], "folderId": null, - "id": "f1d8b375-8b6a-4bb5-9659-520c2864c2e0", + "id": "dced7812-e7a3-4163-b50c-74bee6126553", "identity": null, - "key": "2.H87CevHa/yLkUvgJfpha5g==|IrEdZ2aHOG0P/xCvymr++yzWsMwupJ7Rq0mN9kqTYTc2bsBlpi7/bqva1RhhBRoMuwFnMTx/cX4bMWHZKJbpjV9CENI475Q3JgJScjY/ceQ=|jMosP2JYP9PqRhEODetF2ZNuGkEgLuIR+6jy1ILosk0=", + "key": "2.yoo9dZOltLBIDjLudHfx+A==|jNRvIbWF+7FjAq0DcB00oYiGOSKH9DdRk4y1pbLxvZzTaAL637831eiru9Prq6jUdjDdryvqxQFAOm8sRO40s+VVUBdI48ILc/WO+FUT7do=|QAgU5av2dp8zRmbWdxbullH//bo0IdNS8u3JAG+/eJE=", "login": { "uri": null, - "username": "2.2slHKGwhT9pYZNuZJZELtQ==|2S8vFbqTdE2O5jghb5fIPg==|wUYsi6TFyOdFL1nJG16nO3FMRH4QAtVyNb1vzchcIg8=" + "username": "2./SO1TFQJ9BxFipSJIuhVeg==|VapfwOlRcxVnNj+5Tz8YLg==|0MJNPVx43qXBou7o84jccWRumDG+GQqkxexbhArGUFc=" }, - "name": "2.1r73Ar0+UEKhQ6/HJlgy6w==|JJ9AyivpozzvKk5fdyHirGbsDQI1kwlVr6xZoAZt6dg=|5Xy/XeqN6YdLKC4dVCECydxJbBTonMq9tq6e0/Y0bGw=", + "name": "2.w4kzDaZLWBQLwFMffQUc4Q==|gwtAiY/86hyH/kh/diHlJBQ6wlQZc4xMrpHvkVzdGp8=|gqF2h3FN9S5hiDK1kASO8wBjNp8ognh8w25tsKfA+Go=", "notes": null, "object": "cipherDetails", "organizationId": null, "organizationUseTotp": true, "passwordHistory": [], "reprompt": 0, - "revisionDate": "2024-10-02T07:23:54.495998Z", + "revisionDate": "2024-10-02T07:42:54.189295Z", "secureNote": null, "type": 1, "viewPassword": true diff --git a/internal/bitwarden/embedded/fixtures/test-kdf0_POST_identity_connect_token.json b/internal/bitwarden/embedded/fixtures/test-kdf0_POST_identity_connect_token.json index 86bce5f..53e60e7 100644 --- a/internal/bitwarden/embedded/fixtures/test-kdf0_POST_identity_connect_token.json +++ b/internal/bitwarden/embedded/fixtures/test-kdf0_POST_identity_connect_token.json @@ -1,23 +1,14 @@ { - "ForcePasswordReset": false, "Kdf": 0, "KdfIterations": 600000, "KdfMemory": 0, "KdfParallelism": 0, "Key": "2.fl6CmmK/o/THYg8Y3Z5fJw==|sEjr1DMSe+Hgg7DRU8Z2wqZiZWIySrY63E8ISwQ7Q9vpxUPDQPifTW9oW0ZjcnKE4Xl4fxP89LO5xce+y+yR1yAQpnVcm2wMBx0OXM1xaAU=|9wNu2sNXt7ECSXCYsDL1ctWw02GGT1y7Pkx2NxIyD0A=", - "MasterPasswordPolicy": { - "object": "masterPasswordPolicy" - }, "PrivateKey": "2.11j1IvphccY0UkZov7ZbmQ==|4J34a16NM+krEXyDUkHVUNL2sSA7dph4EBCLWoDkmDoljqMZh5IxA7M51yZTNB6/uqGgJZJX0uU+B4RSn2s2JoFG/VizMmgSIJkTIr6WuA7taEzyIFRi7sY980W02pnLeb8fGV8EXrZk1Bj0UzAFukNGoeS2CHWbm0bZKRvDxl5Vw40V3Lt6GzHTb6X+4k4Ovg0UKgm2mqMWzxxjmCfy+G1R0vlsAtqg14n2ax5f3Jn4ajGDvT2BQia9EFYFkie/SfqgsYbtvou4DvyCTEWh0j9Cre1QIJQ9AZ4AHAzGwIKFhpN7sWjsfw6yTGmvxAfflCMFzCdleBBiuCefaAKNe5Uf1QASC9TzIiHknl2+sK9lOFZ+XZJ41HCU2sMvNBu1EHuBPNRIHodCEHdXHyRqBaC+vgWO6RcTWU4K5ZctBKmElAYst7FFCuzM+liUcGp3I1a7wxOJTPraxeFw8z69rYFiNMOng/HkV/G/VUQXzWalC5gtjaR35dMp35Mf1lMlGnWhy/qdLr0NVdl69qUDKt+kCG9+kiXO7Eq5PTbV+tj4AxBzxE1DroAh0jTPFLb7p1205+OQqqrCEq6tzpGHKTFLoyHZCVoxdQvxgFNX6+fNi3+MfbDfLle7msQAwOuaUl6rxcb2JjdgRQQUd4/GQbvKeLjFRIr7OtcVHWm3251jqRLBgFJcQ235SFgdjLnKATqN4BRe0WLyYOMuMQEmfQXaebjzMo7L2JMJeOar9QutuV+Acle7kJZZU4N1XPiUmborUksAcrmLGXvQITn1QLmadou0jX/oov+lkrKDIFTJC5MIutVBNmExS6FqW978viF1ZNgRQxG5OULHoT5tgfyjrl2u74VFf0ttbLC/JSsi1k8me2LA2vi2Sx7DNOzd6fLx0f8+mYmz2VTRw6fnoFh99d8HktPUNyKcGNZyZpsfkkhFSFV/U88tZ9gROWayLbRXS6g801Mi0tBg0RJp/Flo9ZX9Y3qKVWLCDZT6t3l1NR2npkGmi16yqslXSs9hadIvd6lxyYqyW6cskuJjsacHPOCyvz1xZFH6txtzmb46UrKUU7Cjo/09Kmtb740rXzhH/0nPuvJUjlk0UN77LqzvnvVWAe2IJVEjsbhTspLw3Z1fmCQxMKvc3yPA36/KP2uk/wOlHmBnuX/eebQmI22PFOzskl1d0ScNItJsw5D6fkEhybxEY3oPWYG5y+Oitqj1dQy/TNKXeJHMP+MC1PxzvaE3jq/+cb0IwVBnMVblbaHCJM6JAllV/p3Z6/Ho3Ps4hDHRflKZjP2HyB5wvvTtYhX5XpDoA8s9e6TpDXYnoE+XLN4Thzu30GL3WBeI7koYuNEpRhD5++oT2CElpEqo0v/TFKGSmnV+qohWTL8I4WnHSZeKDLC5alqIMYgRkN6qUlVJLzILOFzeJcL9zeXsAeHemd3VZWe0OYiJRxVwBCYGHk6W7WA4pZ+Wq/C0JHaxNJhr9gx0kyFlMPBkwN/LEyUP+PxsqIuW+09f70qZdGE1r9bNjUSKmb53I6nqxwQhIwvpZZDGAOZjaxrfC8PE9rOOTkM3v6zsSWgjl4EJMPwJubHJb63w5hvW2DwS4h3t7lduTab9ykKInvfBsaWxLuBhJuX+fesGTM0KGXs=|BXz0QzgtArYQXICHbYvGqOeFVi9YGZRP0MASNlVwzzY=", "ResetMasterPassword": false, - "UserDecryptionOptions": { - "HasMasterPassword": true, - "Object": "userDecryptionOptions" - }, - "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3Mjc4NTM4MzQsImV4cCI6MTcyNzg2MTAzNCwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdHxsb2dpbiIsInN1YiI6ImFhZjE1YmQxLTRmNTEtNGJhMC1hZGU4LTlkYzJlYzBmZDJjMyIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJ0ZXN0LWtkZjBAbGF2ZXJzZS5uZXQiLCJlbWFpbCI6InRlc3Qta2RmMEBsYXZlcnNlLm5ldCIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJzc3RhbXAiOiJhYTQ4NTQ2My1jMjU4LTQwYjUtYjJjZi1iZjIyYmY3MDUyNTgiLCJkZXZpY2UiOiIiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.HBnh7fQehhK5cVslhxLLckkYhDtY4IFXTTqLMMcB5HzyQtFfFgyllxisQsqnLikGF7W4K-UN_L26h_pWKRzWdywxbNRKAs7Jc6NYeesSXJpsBrC1zqXKv5rWSyM59oqM3fp_owRqftv9ujvKqEn_c-UteVvtmL_ieS66H2tPp8zBT08FK23ra1EhvW9O6UA7Qx8YxrHCnvOIVJ16paiPSjYWBR_p7fz0sBzoz2ypGGQ6UpJJpW60UK4AAT7-DxJe68eRIwHpbLd7ZjR3x53Y9bBI7toZ3SemXP7guz6fWl_xqj7UteB3C1a9ISI7TFCkJUP7BVRNBJbYdRHEzFZxUg", + "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3Mjc4NTQ5NzQsImV4cCI6MTcyNzg2MjE3NCwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdHxsb2dpbiIsInN1YiI6ImFhZjE1YmQxLTRmNTEtNGJhMC1hZGU4LTlkYzJlYzBmZDJjMyIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJ0ZXN0LWtkZjBAbGF2ZXJzZS5uZXQiLCJlbWFpbCI6InRlc3Qta2RmMEBsYXZlcnNlLm5ldCIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJzc3RhbXAiOiJhYTQ4NTQ2My1jMjU4LTQwYjUtYjJjZi1iZjIyYmY3MDUyNTgiLCJkZXZpY2UiOiIiLCJzY29wZSI6WyJhcGkiXSwiYW1yIjpbIkFwcGxpY2F0aW9uIl19.Wz8DpAJrjbCvmFHsYSUht_mPmfylqr7fK83Y7VRBFYY_noLikp3PAlX1DLdTKJhxMItzvD-Lx9L9EtAt1LpRTzYz3Z-977UloJ60lGAp8wBJNp3KgFI-rLzgET4HhuccDdnoIlMUybSmPn0VJ5zrjXIwq2oSxaEPSm9zSrUgI0p7c6feie10faE_6ixyWJFmuAw5U_k2dLLbx3mHqBNdBgyVGMNhrxjSEMmoEfqm-uKjPWF2Cu09NYMGpD_hHkmI4NKwee6LxHWFfaEFQeGZvDqZ-IPNBY0qGg7dc4kun1-6cnDWmPxUIaQemEQWj__STTo6WDpFcinyZQS4Fx7HGQ", "expires_in": 7200, - "refresh_token": "q89oDzmqCjdl4cxLj9RCKjCA8oQtjxZc2vebEWIApputzfVSYjpWJ9Tq_yVNhK0Rd7RJKksg4ycfkMqyVgG2xw==", - "scope": "api offline_access", + "scope": "api", "token_type": "Bearer", "unofficialServer": true } \ No newline at end of file diff --git a/internal/bitwarden/embedded/fixtures/test-kdf1_GET_api_sync.json b/internal/bitwarden/embedded/fixtures/test-kdf1_GET_api_sync.json index 07707ed..ee34cd9 100644 --- a/internal/bitwarden/embedded/fixtures/test-kdf1_GET_api_sync.json +++ b/internal/bitwarden/embedded/fixtures/test-kdf1_GET_api_sync.json @@ -37,6 +37,80 @@ "type": 1, "viewPassword": true }, + { + "attachments": null, + "card": null, + "collectionIds": [], + "creationDate": "2024-10-02T07:42:54.536202Z", + "data": { + "fields": [], + "name": "2.lSAI94z++lxjXV7F60wD8A==|c02oRIWPKahtdY0/fRUUaWmEr5AvLGrycczUq66jg+0=|Oq9Qhqd1rFqD+hU+O+pkx94PgoJMfJf20mBHVY7DReQ=", + "notes": null, + "passwordHistory": [], + "uri": null, + "username": "2./yXF39ra7G1lu1+bi6YpMg==|7mFeZwcbWq5ZxTSywQx88A==|SkefRdevf6PMxJ73nyfQsRgPwmebcZOxBc3L2dCnanA=" + }, + "deletedDate": null, + "edit": true, + "favorite": false, + "fields": [], + "folderId": null, + "id": "8c071b82-5c79-47b5-8153-b37186daa5a2", + "identity": null, + "key": "2.H/woeJP3IG0/0MUun7vcUQ==|661oI8gy9efZtIHvXYhNwmr3fz0gwLm4LZwMsJUc1Ek+Ooq7wKfc/yTg+TLDY6ScDebhxDD6SAlRbgoBTbcTiF5CYB8ax2wUmNBpSPKYEoY=|aHEWPgao+Wo3703r+yV5scPzZnEFkqOt3snsIzDUKv8=", + "login": { + "uri": null, + "username": "2./yXF39ra7G1lu1+bi6YpMg==|7mFeZwcbWq5ZxTSywQx88A==|SkefRdevf6PMxJ73nyfQsRgPwmebcZOxBc3L2dCnanA=" + }, + "name": "2.lSAI94z++lxjXV7F60wD8A==|c02oRIWPKahtdY0/fRUUaWmEr5AvLGrycczUq66jg+0=|Oq9Qhqd1rFqD+hU+O+pkx94PgoJMfJf20mBHVY7DReQ=", + "notes": null, + "object": "cipherDetails", + "organizationId": null, + "organizationUseTotp": true, + "passwordHistory": [], + "reprompt": 0, + "revisionDate": "2024-10-02T07:42:54.536333Z", + "secureNote": null, + "type": 1, + "viewPassword": true + }, + { + "attachments": null, + "card": null, + "collectionIds": [], + "creationDate": "2024-10-02T07:42:36.834042Z", + "data": { + "fields": [], + "name": "2.b17ROLQsbMRSTrytcX5KSQ==|gEPPxcj3aUUYuK0PU3wjYlvrr+rvSbZf31JIkKJ6lrU=|XiUCDVYvRStdelDdfxe0eKvQE1ind0c+ABk/1cmaync=", + "notes": null, + "passwordHistory": [], + "uri": null, + "username": "2.VZy6ct00c8F40bBg5FhbsQ==|n+Lktw7gvd0Xr2JHpIIXGw==|HbbKTu0Ipvp+MN41t2PcdL73iWhLx0T7Tiu9piz1BXM=" + }, + "deletedDate": null, + "edit": true, + "favorite": false, + "fields": [], + "folderId": null, + "id": "9e51deed-db2f-4435-9b2e-ceed489d98f1", + "identity": null, + "key": "2.8cSTghkv0MsInRBfYs8Irg==|rFmXUMalI9nENKLQOwrI4ixcdqeAU7/KjZa0Ib+a0G5eGARNWPQ06C1RrA5Ao4rBw/Sy5hIteEAj3xXi6rSGzG5gON6oj5W1sm/OFnLJAYQ=|U+PgB4Tt1b+VWpI7kvDUz3p8KJPNuH6TV6/o9iNNtOw=", + "login": { + "uri": null, + "username": "2.VZy6ct00c8F40bBg5FhbsQ==|n+Lktw7gvd0Xr2JHpIIXGw==|HbbKTu0Ipvp+MN41t2PcdL73iWhLx0T7Tiu9piz1BXM=" + }, + "name": "2.b17ROLQsbMRSTrytcX5KSQ==|gEPPxcj3aUUYuK0PU3wjYlvrr+rvSbZf31JIkKJ6lrU=|XiUCDVYvRStdelDdfxe0eKvQE1ind0c+ABk/1cmaync=", + "notes": null, + "object": "cipherDetails", + "organizationId": null, + "organizationUseTotp": true, + "passwordHistory": [], + "reprompt": 0, + "revisionDate": "2024-10-02T07:42:36.834178Z", + "secureNote": null, + "type": 1, + "viewPassword": true + }, { "attachments": null, "card": null, diff --git a/internal/bitwarden/embedded/fixtures/test-kdf1_POST_api_accounts_api-key.json b/internal/bitwarden/embedded/fixtures/test-kdf1_POST_api_accounts_api-key.json new file mode 100644 index 0000000..e191c48 --- /dev/null +++ b/internal/bitwarden/embedded/fixtures/test-kdf1_POST_api_accounts_api-key.json @@ -0,0 +1,5 @@ +{ + "apiKey": "oQAvXGx5h3iw0wzzgRwySsGxn3PvvA", + "object": "apiKey", + "revisionDate": "2024-10-02T07:42:36.834275Z" +} \ No newline at end of file diff --git a/internal/bitwarden/embedded/fixtures/test-kdf1_POST_api_ciphers.json b/internal/bitwarden/embedded/fixtures/test-kdf1_POST_api_ciphers.json index fda609c..d5d2201 100644 --- a/internal/bitwarden/embedded/fixtures/test-kdf1_POST_api_ciphers.json +++ b/internal/bitwarden/embedded/fixtures/test-kdf1_POST_api_ciphers.json @@ -2,35 +2,35 @@ "attachments": null, "card": null, "collectionIds": [], - "creationDate": "2024-10-02T07:23:54.690520Z", + "creationDate": "2024-10-02T07:42:54.536202Z", "data": { "fields": [], - "name": "2.80lmt4GKwJjYlPijpkJOOw==|l0q9zQIDd6OJ5vcYnAeRDP3cFbQn9eOvE23nIJW+pJ0=|AT2gSy0ULDxad9nO5NfEYVacnFd5d1M3AYd3rT6PPX0=", + "name": "2.lSAI94z++lxjXV7F60wD8A==|c02oRIWPKahtdY0/fRUUaWmEr5AvLGrycczUq66jg+0=|Oq9Qhqd1rFqD+hU+O+pkx94PgoJMfJf20mBHVY7DReQ=", "notes": null, "passwordHistory": [], "uri": null, - "username": "2.C5tIDHGKYVFELsvnp9Gs4Q==|AjbBGTEPEzF7p3TDiaDihA==|NZZksTRi8OU2QtqaTx9RNnGUfa7tOb3N/i2rs08EWmU=" + "username": "2./yXF39ra7G1lu1+bi6YpMg==|7mFeZwcbWq5ZxTSywQx88A==|SkefRdevf6PMxJ73nyfQsRgPwmebcZOxBc3L2dCnanA=" }, "deletedDate": null, "edit": true, "favorite": false, "fields": [], "folderId": null, - "id": "30b00bfa-a32f-466a-9f23-dbc312455790", + "id": "8c071b82-5c79-47b5-8153-b37186daa5a2", "identity": null, - "key": "2.EFS7b18DYHnPgrK8tI0crw==|Ce+Dek2JYNCk05ijSYm78WS0+Y8odUeNDQsrAnkXllMAc4NG1FfCOhcfT2CHBlQD/YudriUvyhrTvWI/5GQIu9sCHRhEUYQE7o9yGOh/VmQ=|jescgRjRQqz+K6OSlVgNqjTJ6go3khELIm+TKfxE4gQ=", + "key": "2.H/woeJP3IG0/0MUun7vcUQ==|661oI8gy9efZtIHvXYhNwmr3fz0gwLm4LZwMsJUc1Ek+Ooq7wKfc/yTg+TLDY6ScDebhxDD6SAlRbgoBTbcTiF5CYB8ax2wUmNBpSPKYEoY=|aHEWPgao+Wo3703r+yV5scPzZnEFkqOt3snsIzDUKv8=", "login": { "uri": null, - "username": "2.C5tIDHGKYVFELsvnp9Gs4Q==|AjbBGTEPEzF7p3TDiaDihA==|NZZksTRi8OU2QtqaTx9RNnGUfa7tOb3N/i2rs08EWmU=" + "username": "2./yXF39ra7G1lu1+bi6YpMg==|7mFeZwcbWq5ZxTSywQx88A==|SkefRdevf6PMxJ73nyfQsRgPwmebcZOxBc3L2dCnanA=" }, - "name": "2.80lmt4GKwJjYlPijpkJOOw==|l0q9zQIDd6OJ5vcYnAeRDP3cFbQn9eOvE23nIJW+pJ0=|AT2gSy0ULDxad9nO5NfEYVacnFd5d1M3AYd3rT6PPX0=", + "name": "2.lSAI94z++lxjXV7F60wD8A==|c02oRIWPKahtdY0/fRUUaWmEr5AvLGrycczUq66jg+0=|Oq9Qhqd1rFqD+hU+O+pkx94PgoJMfJf20mBHVY7DReQ=", "notes": null, "object": "cipherDetails", "organizationId": null, "organizationUseTotp": true, "passwordHistory": [], "reprompt": 0, - "revisionDate": "2024-10-02T07:23:54.690612Z", + "revisionDate": "2024-10-02T07:42:54.536333Z", "secureNote": null, "type": 1, "viewPassword": true diff --git a/internal/bitwarden/embedded/fixtures/test-kdf1_POST_identity_connect_token.json b/internal/bitwarden/embedded/fixtures/test-kdf1_POST_identity_connect_token.json index 469ed03..fe82f6b 100644 --- a/internal/bitwarden/embedded/fixtures/test-kdf1_POST_identity_connect_token.json +++ b/internal/bitwarden/embedded/fixtures/test-kdf1_POST_identity_connect_token.json @@ -1,23 +1,14 @@ { - "ForcePasswordReset": false, "Kdf": 1, "KdfIterations": 3, "KdfMemory": 64, "KdfParallelism": 4, "Key": "2.Q+HxT8fkXKmM+irKC+1D6g==|hxmjLJ2RZUPM2uWk0gmPmNe0/OQrCbB49hKUIoqvXWbUaxricj62fATs81UZaKQoC5Re1LPyFsioRImDl1L+sv+EcBEhOx/0wqZ+JPNxXms=|t0G4kLfldPtNCg3wzXU81olAxgbe6sZHAMlQkjjJLck=", - "MasterPasswordPolicy": { - "object": "masterPasswordPolicy" - }, "PrivateKey": "2.fRqWht1SVjfmdTOYbLxbvw==|6nMLdGVZmXlnSjmDyJC94ljd/OYtCTflhVew0+uB4smS2YfFdmkfely/ZMU5UyDyuTCMp5ynw0ERTm3whVmwNGZkYHYT1iuU8I+N5zSQyuNPqnTREGFVslwcMSqkgINYUNoTnJYr/YkLjqzBLt4R5AvFY9Pxq20c+u+3obGcXVW2I11cxF47HVQde9pks72EvOw8KAQs6nQxx8CS/hWLmpZCh/f9yZ0PtNoeik+NxsnSI9veaLvr9yUwqWyhpF1+EI+PMpYZzUopyue8c0aihEUe5Pthz5z1KU+JXMWAAvtg/6eYFi8fQqT8Y6BOZh07makHPYIePQ2O/vgAETldiSlWwmjczZCQ5OXE4wromnzxKBYu1/K2uFUuQ0CD+xsleZImAyYR1W2zVslLFb/ILUX0RSKuqA2MlZizRldTY4nZ7quOWfHP0YD6iRK3oGFf7FfYPAPlcPOPDKl1tEhel3a3M8R0mkyb2e2S6IoW0hfYE4V4J0+VEmdNu7LQqFleqpEPl7icG0Z0EpP3KIJ4OcnO3gbUyo83MOyq4TQMIl46BQt42R4NcoA4hedV/YblUwnri58ugKfURH4+iSjv/r+F2uoSzJu2vdVHka3lF30bwhemyEUhsOZtHaEXpasKHesMknTv4/e+FhAdE9hjplWQyXO7rATXH6F0nEbyCDfOkghZAiAboTjBcE1FoqXBdCSkn4uVzmeWIGJp7izqzM/8zLK2VPUAMtxJGh9+rcv27bLYsbHfcnvEhCyXEsxa0b3DSGijyeSQC2yBNqJen9/SHbsCGNDOLNNyzYUzfIepQ52Oy2LgJEZ57ODTUybPfX4tyW8b+NriSzkUsx9Lp6hh/UaF6f+hBcQ7/dWVoHTJFyUHB1eni0PfFot0dXusg4n7usaG4e4ZSDQprgPHCyg+c3IxauHQmh9/oq1C4/5KuXIB/vOtMq98xjwnZTRdiJxOS82UyqpnwYcGQ9W2RIqKt1GXo/R5rgPXXtgmssqClu7QKI4sHA5lzTs1ef5BHFW1zAYPmsFvovTdX8ckrSdAoeQHJlIb4WyCNB4swNhmMFeQpdgCAZsBAjr+GG6Ak0JxXa2eQBF3zs0f37ly0t3BV7cCuDYKKSol5q14zTQkSQNg+/7TmhtmJo9ducSNS8orQT5IYA+OJa28wGfXjKKaSu0WSH/Tr83984nOQfUdlUZ2rqiskzW7/KUK2nlUceBty+0npuvrzqKk4eDIlJvh6uqwbU0C6SU9MFa0u4nXzfDvDHOgzuz4mNyCYe9tWzU2qArH9dzYMdc1UVj8qlmrPm2WkuXeEEYO+4MBAEHIuQxs21EshpE+2kePRpE5XHn/NbuZhbZ3j7ZD8w7Pyp4PC+hVAg83P+fvWU5CRuA8Ee2wNE6TORtqjnzW40Uh4JcQCaGwOt7dnDVygOsD9NGQrcrY8rLnK1kFMLJmfJtJ8BAn4A18RscI8OuwBkpfoOe9QldDXYsSAJ40K/yE3Nk25Csb3sgoHttfy43CpIIYvNzrqajZA0IvP22eWiF/FeZKO7zm0Myw33ARyHFw2yiBLNys4xxVuL3YmpolHRzOaUqtWA3bNJTmKWiBXxlJvL8+OvbfY+TYXJdVBUVzaATG4CL546j5hduhZfDxJQk=|1kt/ukROVrAIqC47wt8TLQYVh9jeZaCUE3/hHo8hCNA=", "ResetMasterPassword": false, - "UserDecryptionOptions": { - "HasMasterPassword": true, - "Object": "userDecryptionOptions" - }, - "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3Mjc4NTM4MzQsImV4cCI6MTcyNzg2MTAzNCwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdHxsb2dpbiIsInN1YiI6IjNmMGFiZjE3LWU3NzktNDMxMi1hM2RkLTljNjI2NmU5NWE5ZSIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJ0ZXN0LWtkZjFAbGF2ZXJzZS5uZXQiLCJlbWFpbCI6InRlc3Qta2RmMUBsYXZlcnNlLm5ldCIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJzc3RhbXAiOiJlOTY4YmJkMS05ZjY1LTQ2ZWYtYjE0ZS0wZDU1N2IzYjNiMWEiLCJkZXZpY2UiOiIiLCJzY29wZSI6WyJhcGkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiQXBwbGljYXRpb24iXX0.gbUXx7PzaRxB6DkMwHiVWHpFw_RC2Ztow3FpKct1mXI1gc3drMWqA8sQJtLu2wnslaD2pTYECcBejLFpgU2z4Z-homCbWxL9kr5O_3GkR_4RId9iVVY2P-TY-wX4GRY7IpQCPzGLUSL1cqSjlLx4cLIr6nmEiySQj2kvnR7ce3xqcb0rMeZMm72lC9UySP_VdlYE6nYEvNGVhstLfLSx44SBIhRzSGXYtezgyE-RBEcH1uv8WzUimfnhb_KoyAmxu9RaYffzFF-J6PFHswQt7tbbl5CYK_jpNvptaBBcyesuM_QpwuJNAyDovncDgHfFwMzNpuwW86i-wfEpWsnoNQ", + "access_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJuYmYiOjE3Mjc4NTQ5NzQsImV4cCI6MTcyNzg2MjE3NCwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdHxsb2dpbiIsInN1YiI6IjNmMGFiZjE3LWU3NzktNDMxMi1hM2RkLTljNjI2NmU5NWE5ZSIsInByZW1pdW0iOnRydWUsIm5hbWUiOiJ0ZXN0LWtkZjFAbGF2ZXJzZS5uZXQiLCJlbWFpbCI6InRlc3Qta2RmMUBsYXZlcnNlLm5ldCIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJzc3RhbXAiOiJlOTY4YmJkMS05ZjY1LTQ2ZWYtYjE0ZS0wZDU1N2IzYjNiMWEiLCJkZXZpY2UiOiIiLCJzY29wZSI6WyJhcGkiXSwiYW1yIjpbIkFwcGxpY2F0aW9uIl19.M4v3fwEttaX3HKV0AihjN5CfjdetaYFLrxQEgTkMEcEJsxJzIAIqzLqOttRI2GKqHdE8Bm8uyIXIppFQ0tPZ1A7l-pqLv6otm30xf9SAII5NfKDkLHWtRbZN5TSatPqlT44TSkUc0oVcEmYVPToWbaWOfs2u_-7d6y2QzMH8gmMtCoDM4X6mW_aY00I88iPrPzfoCCjUyFnvE8l6Q0Ni00MUWuVbGZOjWMPwLxjDaXRJKjxKRhFUKYiqDodaX_M11xoElqOsPYKXvwzqOMaIxhY52b5suxnRneLdbruuKEKyZisg7dELn3EBhPCWBwsLKSyDEC63JrX8DiL1Lypyfg", "expires_in": 7200, - "refresh_token": "jRlQnvOEIPacYzAFp5gmFEDVfBFE0gqYpdgok-PZtS1Vs5JNIf84KCKrAOi-SUtwL-snjCRnnychciR2k3C3Ng==", - "scope": "api offline_access", + "scope": "api", "token_type": "Bearer", "unofficialServer": true } \ No newline at end of file diff --git a/internal/bitwarden/embedded/vault_webapi.go b/internal/bitwarden/embedded/vault_webapi.go index c6d1116..7ea4ef6 100644 --- a/internal/bitwarden/embedded/vault_webapi.go +++ b/internal/bitwarden/embedded/vault_webapi.go @@ -25,6 +25,7 @@ type WebAPIVault interface { DeleteAttachment(ctx context.Context, itemId, attachmentId string) error DeleteObject(ctx context.Context, obj models.Object) error EditObject(ctx context.Context, obj models.Object) (*models.Object, error) + GetAPIKey(ctx context.Context, username, password string) (*models.ApiKey, error) GetAttachment(ctx context.Context, itemId, attachmentId string) ([]byte, error) LoginWithAPIKey(ctx context.Context, password, clientId, clientSecret string) error LoginWithPassword(ctx context.Context, username, password string) error @@ -416,6 +417,20 @@ func (v *webAPIVault) EditObject(ctx context.Context, obj models.Object) (*model return resObj, nil } +func (v *webAPIVault) GetAPIKey(ctx context.Context, username, password string) (*models.ApiKey, error) { + resp, err := v.client.GetAPIKey(ctx, username, password, v.loginAccount.KdfConfig) + if err != nil { + return nil, fmt.Errorf("error getting API key: %w", err) + } + + apiKey := &models.ApiKey{ + ClientID: fmt.Sprintf("user.%s", v.loginAccount.AccountUUID), + ClientSecret: resp.ApiKey, + } + + return apiKey, nil +} + func (v *webAPIVault) GetAttachment(ctx context.Context, itemId, attachmentId string) ([]byte, error) { if v.locked { return nil, models.ErrVaultLocked diff --git a/internal/bitwarden/embedded/vault_webapi_test.go b/internal/bitwarden/embedded/vault_webapi_test.go index 4012729..9f7752a 100644 --- a/internal/bitwarden/embedded/vault_webapi_test.go +++ b/internal/bitwarden/embedded/vault_webapi_test.go @@ -29,6 +29,24 @@ func TestLoginAsPasswordLoadsAccountInformationForPbkdf2(t *testing.T) { assert.Equal(t, fixtures.Pdkdf2ProtectedSymmetricKey, vault.loginAccount.ProtectedSymmetricKey) } +func TestLoginAsAPILoadsAccountInformationForPbkdf2(t *testing.T) { + vault, reset := newMockedWebAPIVault(fixtures.MockedClient(t, fixtures.Pdkdf2Mocks)) + defer reset() + + ctx := context.Background() + err := vault.LoginWithAPIKey(ctx, fixtures.TestPassword, "user.aaf15bd1-4f51-4ba0-ade8-9dc2ec0fd2c3", "ZTXHHyPY6bNlNq1diDA2nM1GROboP3") + if err != nil { + t.Fatalf("vault unlock failed: %v", err) + } + + assert.Equal(t, "API", vault.loginAccount.VaultFormat) + assert.Equal(t, fixtures.Pdkdf2Email, vault.loginAccount.Email) + assert.Equal(t, models.KdfTypePBKDF2_SHA256, vault.loginAccount.KdfConfig.KdfType) + assert.Equal(t, 600000, vault.loginAccount.KdfConfig.KdfIterations) + assert.Equal(t, fixtures.Pdkdf2ProtectedRSAPrivateKey, vault.loginAccount.ProtectedRSAPrivateKey) + assert.Equal(t, fixtures.Pdkdf2ProtectedSymmetricKey, vault.loginAccount.ProtectedSymmetricKey) +} + func TestLoginAsPasswordLoadsAccountInformationForArgon2(t *testing.T) { vault, reset := newMockedWebAPIVault(fixtures.MockedClient(t, fixtures.Argon2Mocks)) defer reset() @@ -47,6 +65,24 @@ func TestLoginAsPasswordLoadsAccountInformationForArgon2(t *testing.T) { assert.Equal(t, fixtures.Argon2ProtectedSymmetricKey, vault.loginAccount.ProtectedSymmetricKey) } +func TestLoginAsAPILoadsAccountInformationForArgon2(t *testing.T) { + vault, reset := newMockedWebAPIVault(fixtures.MockedClient(t, fixtures.Argon2Mocks)) + defer reset() + + ctx := context.Background() + err := vault.LoginWithAPIKey(ctx, fixtures.TestPassword, "user.3f0abf17-e779-4312-a3dd-9c6266e95a9e", "oQAvXGx5h3iw0wzzgRwySsGxn3PvvA") + if err != nil { + t.Fatalf("vault unlock failed: %v", err) + } + + assert.Equal(t, "API", vault.loginAccount.VaultFormat) + assert.Equal(t, fixtures.Argon2Email, vault.loginAccount.Email) + assert.Equal(t, models.KdfTypeArgon2, vault.loginAccount.KdfConfig.KdfType) + assert.Equal(t, 3, vault.loginAccount.KdfConfig.KdfIterations) + assert.Equal(t, fixtures.Argon2ProtectedRSAPrivateKey, vault.loginAccount.ProtectedRSAPrivateKey) + assert.Equal(t, fixtures.Argon2ProtectedSymmetricKey, vault.loginAccount.ProtectedSymmetricKey) +} + func TestObjectCreation(t *testing.T) { vault, reset := newMockedWebAPIVault(fixtures.MockedClient(t, fixtures.Pdkdf2Mocks)) defer reset() diff --git a/internal/bitwarden/models/models.go b/internal/bitwarden/models/models.go index 63a1ff7..1fa6bd2 100644 --- a/internal/bitwarden/models/models.go +++ b/internal/bitwarden/models/models.go @@ -58,6 +58,7 @@ const ( ObjectTypeProfileOrganization ObjectType = "profileOrganization" // organization under profile ObjectCipherDetails ObjectType = "cipherDetails" // when creating attachment data ObjectAttachmentFileUpload ObjectType = "attachment-fileUpload" // when creating attachment data + ObjectApiKey ObjectType = "api-key" ) const ( @@ -95,6 +96,11 @@ type SecureNote struct { Type int `json:"type,omitempty"` } +type ApiKey struct { + ClientID string + ClientSecret string +} + type Object struct { Attachments []Attachment `json:"attachments,omitempty"` Card []byte `json:"-"` diff --git a/internal/bitwarden/webapi/client.go b/internal/bitwarden/webapi/client.go index 2286bda..74774e7 100644 --- a/internal/bitwarden/webapi/client.go +++ b/internal/bitwarden/webapi/client.go @@ -37,6 +37,7 @@ type Client interface { EditFolder(ctx context.Context, obj Folder) (*Folder, error) EditObject(context.Context, models.Object) (*models.Object, error) EditOrgCollection(ctx context.Context, orgId, objId string, obj OrganizationCreationRequest) (*Collection, error) + GetAPIKey(ctx context.Context, username, password string, kdfConfig models.KdfConfiguration) (*ApiKey, error) GetCollections(ctx context.Context, orgID string) ([]CollectionResponseItem, error) GetContentFromURL(ctx context.Context, url string) ([]byte, error) GetObjectAttachment(ctx context.Context, itemId, attachmentId string) (*models.Attachment, error) @@ -243,6 +244,26 @@ func (c *client) GetContentFromURL(ctx context.Context, url string) ([]byte, err return []byte(*resp), err } +func (c *client) GetAPIKey(ctx context.Context, username, password string, kdfConfig models.KdfConfiguration) (*ApiKey, error) { + type ApiKeyRequest struct { + MasterPasswordHash string `json:"masterPasswordHash"` + } + + preloginKey, err := keybuilder.BuildPreloginKey(password, username, kdfConfig) + if err != nil { + return nil, fmt.Errorf("error building prelogin key: %w", err) + } + + hashedPassword := crypto.HashPassword(password, *preloginKey, false) + obj := ApiKeyRequest{MasterPasswordHash: hashedPassword} + httpReq, err := c.prepareRequest(ctx, "POST", fmt.Sprintf("%s/api/accounts/api-key", c.serverURL), obj) + if err != nil { + return nil, fmt.Errorf("error preparing api key retrieval request: %w", err) + } + + return doRequest[ApiKey](ctx, c.httpClient, httpReq) +} + func (c *client) GetCollections(ctx context.Context, orgID string) ([]CollectionResponseItem, error) { httpReq, err := c.prepareRequest(ctx, "GET", fmt.Sprintf("%s/api/organizations/%s/collections", c.serverURL, orgID), nil) if err != nil { diff --git a/internal/bitwarden/webapi/models.go b/internal/bitwarden/webapi/models.go index 1d72a2a..23691b1 100644 --- a/internal/bitwarden/webapi/models.go +++ b/internal/bitwarden/webapi/models.go @@ -130,6 +130,12 @@ type SyncResponse struct { Profile Profile `json:"profile"` } +type ApiKey struct { + ApiKey string `json:"apiKey"` + Object models.ObjectType `json:"object"` + RevisionDate *time.Time `json:"revisionDate"` +} + type Folder struct { Id string `json:"id"` Name string `json:"name"`