From 156730277ae1cf69dd4cefc1c0244e198dc739ea Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 16 Jan 2025 13:21:35 -0800 Subject: [PATCH 01/51] chore(deps): Bump `keyfactor-auth-client-go` to `v1.1.2` --- v3/go.mod | 2 +- v3/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/v3/go.mod b/v3/go.mod index 9fa544d..372fe63 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -19,7 +19,7 @@ go 1.23 toolchain go1.23.2 require ( - github.com/Keyfactor/keyfactor-auth-client-go v1.1.1-rc.0 + github.com/Keyfactor/keyfactor-auth-client-go v1.1.2 github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/spbsoluble/go-pkcs12 v0.3.3 go.mozilla.org/pkcs7 v0.9.0 diff --git a/v3/go.sum b/v3/go.sum index 67d7ee9..dd025be 100644 --- a/v3/go.sum +++ b/v3/go.sum @@ -14,8 +14,8 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 h1:kYRSnvJju5gYVyhkij+RTJ/VR6QIUaCfWeaFm2ycsjQ= github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= -github.com/Keyfactor/keyfactor-auth-client-go v1.1.1-rc.0 h1:/N/7pBj/oTUM1cYga2NvKyA4q6nfE0acciJHZqKC9Ug= -github.com/Keyfactor/keyfactor-auth-client-go v1.1.1-rc.0/go.mod h1:yw92P9gSYVEyWkiUAJFsb7hjhXa8slN1+yTQgjSgovM= +github.com/Keyfactor/keyfactor-auth-client-go v1.1.2 h1:o/BCmICXUErZcodteat3/jrMjzkh8hlNI5kzXB+wsSw= +github.com/Keyfactor/keyfactor-auth-client-go v1.1.2/go.mod h1:yw92P9gSYVEyWkiUAJFsb7hjhXa8slN1+yTQgjSgovM= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= From 278bfdcea981a0e2c0ebee409f9a5be51a04a429 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 16 Jan 2025 13:23:46 -0800 Subject: [PATCH 02/51] chore(deps): ``` go: upgraded github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 => v1.17.0 go: upgraded github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 => v1.8.1 go: upgraded github.com/fatih/color v1.13.0 => v1.18.0 go: upgraded github.com/hashicorp/go-hclog v1.5.0 => v1.6.3 go: upgraded github.com/mattn/go-colorable v0.1.13 => v0.1.14 go: upgraded github.com/mattn/go-isatty v0.0.19 => v0.0.20 go: upgraded golang.org/x/crypto v0.30.0 => v0.32.0 go: upgraded golang.org/x/net v0.32.0 => v0.34.0 go: upgraded golang.org/x/oauth2 v0.24.0 => v0.25.0 go: upgraded golang.org/x/sys v0.28.0 => v0.29.0 ``` --- v3/go.mod | 20 ++++++++++---------- v3/go.sum | 49 ++++++++++++++++++++++++------------------------- 2 files changed, 34 insertions(+), 35 deletions(-) diff --git a/v3/go.mod b/v3/go.mod index 372fe63..9dcc91a 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -26,24 +26,24 @@ require ( ) require ( - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 // indirect - github.com/fatih/color v1.13.0 // indirect + github.com/fatih/color v1.18.0 // indirect github.com/golang-jwt/jwt/v5 v5.2.1 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/hashicorp/go-hclog v1.5.0 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/kylelemons/godebug v1.1.0 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect - golang.org/x/crypto v0.30.0 // indirect - golang.org/x/net v0.32.0 // indirect - golang.org/x/oauth2 v0.24.0 // indirect - golang.org/x/sys v0.28.0 // indirect + golang.org/x/crypto v0.32.0 // indirect + golang.org/x/net v0.34.0 // indirect + golang.org/x/oauth2 v0.25.0 // indirect + golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.21.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/v3/go.sum b/v3/go.sum index dd025be..c4f0ef2 100644 --- a/v3/go.sum +++ b/v3/go.sum @@ -1,9 +1,9 @@ -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 h1:JZg6HRh6W6U4OLl6lk7BZ7BLisIzM9dG1R50zUk9C/M= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0/go.mod h1:YL1xnZ6QejvQHWJrX/AvhFl4WW4rqHVoKspWNVwFk0M= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 h1:B/dfvscEQtew9dVuoxqxrUKKv8Ih2f55PydknDamU+g= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0/go.mod h1:fiPSssYvltE08HJchL04dOy+RD4hgrjph0cwGGMntdI= -github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0 h1:+m0M/LFxN43KvULkDNfdXOgrjtg6UYJPFBJyuEcRCAw= -github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0/go.mod h1:PwOyop78lveYMRs6oCxjiVyBdyCgIYH6XHIVZO9/SFQ= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1 h1:1mvYtZfWQAnwNah/C+Z+Jb9rQH95LPE2vlmMuWAHJk8= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1/go.mod h1:75I/mXtme1JyWFtz8GocPHVFyH421IBoZErnO16dd0k= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.1 h1:Bk5uOhSAenHyR5P61D/NzeQCv+4fEVV8mOkJ82NqpWw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.1/go.mod h1:QZ4pw3or1WPmRBxf0cHd1tknzrT54WPBOQoGutCPvSU= github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.0 h1:WLUIpeyv04H0RCcQHaA4TNoyrQ39Ox7V+re+iaqzTe0= @@ -23,16 +23,17 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= -github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= -github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs= @@ -45,21 +46,20 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0 github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/redis/go-redis/v9 v9.6.1 h1:HHDteefn6ZkTtY5fGUE8tj8uy85AHk6zP7CpzIAM0y4= -github.com/redis/go-redis/v9 v9.6.1/go.mod h1:0C0c6ycQsdpVNQpxb1njEQIqkx5UcsM8FJCQLgE9+RA= +github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= +github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/spbsoluble/go-pkcs12 v0.3.3 h1:3nh7IKn16RDpmrSMtOu1JvbB0XHYq1j+IsICdU1c7J4= @@ -70,22 +70,21 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.mozilla.org/pkcs7 v0.9.0 h1:yM4/HS9dYv7ri2biPtxt8ikvB37a980dg69/pKmS+eI= go.mozilla.org/pkcs7 v0.9.0/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= -golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY= -golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= -golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= -golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= -golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= -golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= +golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= +golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 0bac539dde14e76ed788c3c587abed57e14103cd Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 16 Jan 2025 14:28:05 -0800 Subject: [PATCH 03/51] chore(deps): Bump `github.com/Keyfactor/keyfactor-auth-client-go` to `v1.2.0-rc.0` --- v3/go.mod | 2 +- v3/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/v3/go.mod b/v3/go.mod index 9dcc91a..9e4c372 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -19,7 +19,7 @@ go 1.23 toolchain go1.23.2 require ( - github.com/Keyfactor/keyfactor-auth-client-go v1.1.2 + github.com/Keyfactor/keyfactor-auth-client-go v1.2.0-rc.0 github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/spbsoluble/go-pkcs12 v0.3.3 go.mozilla.org/pkcs7 v0.9.0 diff --git a/v3/go.sum b/v3/go.sum index c4f0ef2..49df6d3 100644 --- a/v3/go.sum +++ b/v3/go.sum @@ -14,8 +14,8 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 h1:kYRSnvJju5gYVyhkij+RTJ/VR6QIUaCfWeaFm2ycsjQ= github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= -github.com/Keyfactor/keyfactor-auth-client-go v1.1.2 h1:o/BCmICXUErZcodteat3/jrMjzkh8hlNI5kzXB+wsSw= -github.com/Keyfactor/keyfactor-auth-client-go v1.1.2/go.mod h1:yw92P9gSYVEyWkiUAJFsb7hjhXa8slN1+yTQgjSgovM= +github.com/Keyfactor/keyfactor-auth-client-go v1.2.0-rc.0 h1:mm5NpyEJhm4em9nn8gWNFtsk211F3IY4oAH8kfZSAqs= +github.com/Keyfactor/keyfactor-auth-client-go v1.2.0-rc.0/go.mod h1:8/msKJyHU7+GwW3n9aYQIyW7Lu5/njaOsFL9dZoQ+Ek= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= From d897eea8aa53cafb76d2df380718b2e99b67b4e2 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Fri, 17 Jan 2025 10:15:44 -0800 Subject: [PATCH 04/51] chore(deps): Bump `github.com/Keyfactor/keyfactor-auth-client-go` to `v1.2.0-rc.5` --- v3/go.mod | 2 +- v3/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/v3/go.mod b/v3/go.mod index 9e4c372..6a0ed78 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -19,7 +19,7 @@ go 1.23 toolchain go1.23.2 require ( - github.com/Keyfactor/keyfactor-auth-client-go v1.2.0-rc.0 + github.com/Keyfactor/keyfactor-auth-client-go v1.2.0-rc.5 github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/spbsoluble/go-pkcs12 v0.3.3 go.mozilla.org/pkcs7 v0.9.0 diff --git a/v3/go.sum b/v3/go.sum index 49df6d3..5656f2b 100644 --- a/v3/go.sum +++ b/v3/go.sum @@ -14,8 +14,8 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 h1:kYRSnvJju5gYVyhkij+RTJ/VR6QIUaCfWeaFm2ycsjQ= github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= -github.com/Keyfactor/keyfactor-auth-client-go v1.2.0-rc.0 h1:mm5NpyEJhm4em9nn8gWNFtsk211F3IY4oAH8kfZSAqs= -github.com/Keyfactor/keyfactor-auth-client-go v1.2.0-rc.0/go.mod h1:8/msKJyHU7+GwW3n9aYQIyW7Lu5/njaOsFL9dZoQ+Ek= +github.com/Keyfactor/keyfactor-auth-client-go v1.2.0-rc.5 h1:3BpabiIcvdSszX7cx7OLBlpS8c7CnmvdquAANOD5+2o= +github.com/Keyfactor/keyfactor-auth-client-go v1.2.0-rc.5/go.mod h1:7htRcBIWn+X4fI5jaYBALSYwP84H/djN7d8y3n0ZDQ0= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= From f50e0a70fab058495ec422268eafb77f8de9357c Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Mon, 3 Mar 2025 12:51:24 -0800 Subject: [PATCH 05/51] fix(certs): When creating subject escape any values that main contain commas. feat(certs): Add `collectionId` support for certificate downloads. --- v3/api/certificate.go | 36 ++++++++++++++++++++++++++++++------ v3/go.mod | 2 +- v3/go.sum | 4 ++-- 3 files changed, 33 insertions(+), 9 deletions(-) diff --git a/v3/api/certificate.go b/v3/api/certificate.go index c734c65..95a7af8 100644 --- a/v3/api/certificate.go +++ b/v3/api/certificate.go @@ -196,6 +196,7 @@ func (c *Client) DownloadCertificate( thumbprint string, serialNumber string, issuerDn string, + collectionId int, ) (*x509.Certificate, []*x509.Certificate, error) { log.Println("[INFO] Downloading certificate") @@ -228,6 +229,19 @@ func (c *Client) DownloadCertificate( ChainOrder: "EndEntityFirst", } + query := apiQuery{ + Query: []StringTuple{}, + } + if collectionId > 0 { + log.Println("[DEBUG] RecoverCertificate: Collection ID:", collectionId) + query.Query = append( + query.Query, StringTuple{ + "collectionId", fmt.Sprintf("%d", collectionId), + }, + ) + log.Println("[DEBUG] RecoverCertificate: Query:", query) + } + // Set Keyfactor-specific headers headers := &apiHeaders{ Headers: []StringTuple{ @@ -242,6 +256,7 @@ func (c *Client) DownloadCertificate( Endpoint: "Certificates/Download", Headers: headers, Payload: payload, + Query: &query, } resp, err := c.sendRequest(keyfactorAPIStruct) @@ -754,30 +769,39 @@ func createSubject(cs CertificateSubject) (string, error) { var subject string if cs.SubjectCommonName != "" && cs.SubjectCommonName != "" { - subject = "CN=" + cs.SubjectCommonName + "," + subject = "CN=" + escapeDNValue(cs.SubjectCommonName) + "," } else { return "", errors.New("build subject: common name required") // Common name is required! } if cs.SubjectOrganizationalUnit != "" && cs.SubjectOrganizationalUnit != "" { - subject += "OU=" + cs.SubjectOrganizationalUnit + "," + subject += "OU=" + escapeDNValue(cs.SubjectOrganizationalUnit) + "," } if cs.SubjectOrganization != "" && cs.SubjectOrganization != "" { - subject += "O=" + cs.SubjectOrganization + "," + subject += "O=" + escapeDNValue(cs.SubjectOrganization) + "," } if cs.SubjectLocality != "" && cs.SubjectLocality != "" { - subject += "L=" + cs.SubjectLocality + "," + subject += "L=" + escapeDNValue(cs.SubjectLocality) + "," } if cs.SubjectState != "" && cs.SubjectState != "" { - subject += "ST=" + cs.SubjectState + "," + subject += "ST=" + escapeDNValue(cs.SubjectState) + "," } if cs.SubjectCountry != "" && cs.SubjectCountry != "" { - subject += "C=" + cs.SubjectCountry + "," + subject += "C=" + escapeDNValue(cs.SubjectCountry) + "," } subject = strings.TrimRight(subject, ",") // remove trailing comma log.Printf("[DEBUG] createSubject(): Certificate subject created: %s\n", subject) return subject, nil } +// escapeDNValue ensures that a value in a DN is properly escaped if it contains special characters. +func escapeDNValue(value string) string { + // If the value contains a comma, quote it + if strings.Contains(value, ",") { + return `"` + value + `"` + } + return value +} + // validateDeployPFXArgs validates the arguments required to deploy a PFX certificate. func validateDeployPFXArgs(dpfxa *DeployPFXArgs) error { if dpfxa.StoreIds == nil { diff --git a/v3/go.mod b/v3/go.mod index 6a0ed78..a2b12ff 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -19,7 +19,7 @@ go 1.23 toolchain go1.23.2 require ( - github.com/Keyfactor/keyfactor-auth-client-go v1.2.0-rc.5 + github.com/Keyfactor/keyfactor-auth-client-go v1.2.0 github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/spbsoluble/go-pkcs12 v0.3.3 go.mozilla.org/pkcs7 v0.9.0 diff --git a/v3/go.sum b/v3/go.sum index 5656f2b..2b13c8a 100644 --- a/v3/go.sum +++ b/v3/go.sum @@ -14,8 +14,8 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 h1:kYRSnvJju5gYVyhkij+RTJ/VR6QIUaCfWeaFm2ycsjQ= github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= -github.com/Keyfactor/keyfactor-auth-client-go v1.2.0-rc.5 h1:3BpabiIcvdSszX7cx7OLBlpS8c7CnmvdquAANOD5+2o= -github.com/Keyfactor/keyfactor-auth-client-go v1.2.0-rc.5/go.mod h1:7htRcBIWn+X4fI5jaYBALSYwP84H/djN7d8y3n0ZDQ0= +github.com/Keyfactor/keyfactor-auth-client-go v1.2.0 h1:uNSlyOW5Bqpi0nsOGZtOYQzN0vP/h4S4J38jtQes+OI= +github.com/Keyfactor/keyfactor-auth-client-go v1.2.0/go.mod h1:7htRcBIWn+X4fI5jaYBALSYwP84H/djN7d8y3n0ZDQ0= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= From a6dff6fd81b29310dd8c671450a38bda37ff58ad Mon Sep 17 00:00:00 2001 From: Macey <11599974+doebrowsk@users.noreply.github.com> Date: Sun, 21 Sep 2025 16:12:17 -0500 Subject: [PATCH 06/51] Expand secret type (#47) --- v3/api/store_models.go | 83 +++++++++++++++++++++++++++++------------- v3/go.mod | 30 +++++++-------- v3/go.sum | 64 ++++++++++++++++---------------- 3 files changed, 104 insertions(+), 73 deletions(-) diff --git a/v3/api/store_models.go b/v3/api/store_models.go index 5938e6a..bf4db2c 100644 --- a/v3/api/store_models.go +++ b/v3/api/store_models.go @@ -51,14 +51,20 @@ type UpdateStoreFctArgs struct { // automatically populated by the CreateStore method. However, if configured, this field will be used. PropertiesString string `json:"Properties,omitempty"` // Mapped name-value pair field used to configure properties. - Properties map[string]interface{} `json:"-"` - AgentId string `json:"AgentId"` - AgentAssigned *bool `json:"AgentAssigned,omitempty"` - ContainerName *string `json:"ContainerName,omitempty"` - InventorySchedule *InventorySchedule `json:"InventorySchedule,omitempty"` - ReEnrollmentStatus *ReEnrollmnentConfig `json:"ReEnrollmentStatus,omitempty"` - SetNewPasswordAllowed *bool `json:"SetNewPasswordAllowed,omitempty"` - Password *StorePasswordConfig `json:"Password"` + Properties map[string]interface{} `json:"-"` + AgentId string `json:"AgentId"` + AgentAssigned *bool `json:"AgentAssigned,omitempty"` + ContainerName *string `json:"ContainerName,omitempty"` + InventorySchedule *InventorySchedule `json:"InventorySchedule,omitempty"` + ReEnrollmentStatus *ReEnrollmnentConfig `json:"ReEnrollmentStatus,omitempty"` + SetNewPasswordAllowed *bool `json:"SetNewPasswordAllowed,omitempty"` + Password *UpdateStorePasswordConfig `json:"Password"` +} + +type UpdateStorePasswordConfig struct { + SecretValue *string `json:"SecretValue"` // used for setting kf-secret value or No Value (null) + Parameters map[string]string `json:"Parameters"` + Provider int `json:"Provider"` } // InventorySchedule holds configuration data for creating an inventory schedule for a certificate store in Keyfactor @@ -94,34 +100,59 @@ type ReEnrollmnentConfig struct { } // StorePasswordConfig configures the password field for a new certificate store. +// TODO: make re-usable struct for Secret type fields type StorePasswordConfig struct { - Value *string `json:"SecretValue"` - SecretTypeGuid *string `json:"SecretTypeGuid,omitempty"` - InstanceId *string `json:"InstanceId,omitempty"` + Value *string `json:"SecretValue"` + SecretTypeGuid *string `json:"SecretTypeGuid,omitempty"` + InstanceId *string `json:"InstanceId,omitempty"` + InstanceGuid *string `json:"InstanceGuid,omitempty"` + ProvidererTypeParameterValues *[]ProviderTypeParameterValue `json:"ProviderTypeParameterValues"` + ProviderId int `json:"ProviderId"` + IsManaged bool `json:"IsManaged"` + HasValue bool `json:"HasValue"` } // ProviderTypeParameterValues - Not yet implemented // ProviderTypeParameterValues ProviderTypeParams - Not implemented /* Future non-critical functionality */ -type ProviderTypeParams struct { - Id string - Value string - InstanceId string - InstanceGuid string - Provider ProviderParams +type ProviderTypeParameterValue struct { + Id int `json:"Id"` + Value *string `json:"Value"` + ParameterId int `json:"ParameterId"` // defaults always to 0, likely deprecated + InstanceId *string `json:"InstanceId"` // defaults null, likely deprecated + InstanceGuid *string `json:"InstanceGuid"` + Provider *string `json:"Provider"` // defaults null, likely deprecated + ProviderTypeParam ProviderTypeParam `json:"ProviderTypeParam"` } -type ProviderParams struct { - Id int - Name string - Area int - ProviderType ProviderType +type ProviderTypeParam struct { + Id int `json:"Id"` + Name *string `json:"Name"` + DisplayName *string `json:"DisplayName"` + DataType int `json:"DataType"` + InstanceLevel bool `json:"InstanceLevel"` + ProviderType *string `json:"ProviderType"` //defaults null, likely deprecated } -type ProviderType struct { - Id string - Name string -} +// type ProviderTypeParams struct { +// Id string +// Value string +// InstanceId string +// InstanceGuid string +// Provider ProviderParams +// } + +// type ProviderParams struct { +// Id int +// Name string +// Area int +// ProviderType ProviderType +// } + +// type ProviderType struct { +// Id string +// Name string +// } // CertStoreTypeResponse contains the response elements returned from the GetCertificateStoreType method. type CertStoreTypeResponse struct { diff --git a/v3/go.mod b/v3/go.mod index 6a0ed78..d69aa62 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -14,36 +14,36 @@ module github.com/Keyfactor/keyfactor-go-client/v3 -go 1.23 +go 1.24 -toolchain go1.23.2 +toolchain go1.24.5 require ( - github.com/Keyfactor/keyfactor-auth-client-go v1.2.0-rc.5 + github.com/Keyfactor/keyfactor-auth-client-go v1.3.0 github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/spbsoluble/go-pkcs12 v0.3.3 go.mozilla.org/pkcs7 v0.9.0 ) require ( - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1 // indirect - github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0 // indirect - github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect github.com/fatih/color v1.18.0 // indirect - github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + github.com/golang-jwt/jwt/v5 v5.2.2 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect - golang.org/x/crypto v0.32.0 // indirect - golang.org/x/net v0.34.0 // indirect - golang.org/x/oauth2 v0.25.0 // indirect - golang.org/x/sys v0.29.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/crypto v0.39.0 // indirect + golang.org/x/net v0.41.0 // indirect + golang.org/x/oauth2 v0.30.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.26.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/v3/go.sum b/v3/go.sum index 5656f2b..2008907 100644 --- a/v3/go.sum +++ b/v3/go.sum @@ -1,21 +1,21 @@ -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 h1:g0EZJwz7xkXQiZAI5xi9f3WWFYBlX1CPTrR+NDToRkQ= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0/go.mod h1:XCW7KnZet0Opnr7HccfUw1PLc4CjHqpcaxW8DHklNkQ= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1 h1:1mvYtZfWQAnwNah/C+Z+Jb9rQH95LPE2vlmMuWAHJk8= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.1/go.mod h1:75I/mXtme1JyWFtz8GocPHVFyH421IBoZErnO16dd0k= -github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.1 h1:Bk5uOhSAenHyR5P61D/NzeQCv+4fEVV8mOkJ82NqpWw= -github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.1/go.mod h1:QZ4pw3or1WPmRBxf0cHd1tknzrT54WPBOQoGutCPvSU= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.0 h1:WLUIpeyv04H0RCcQHaA4TNoyrQ39Ox7V+re+iaqzTe0= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.0/go.mod h1:hd8hTTIY3VmUVPRHNH7GVCHO3SHgXkJKZHReby/bnUQ= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0 h1:eXnN9kaS8TiDwXjoie3hMRLuwdUBUMW9KRgOqB3mCaw= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0/go.mod h1:XIpam8wumeZ5rVMuhdDQLMfIPDf1WO3IzrCRO3e3e3o= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= +github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.1 h1:mrkDCdkMsD4l9wjFGhofFHFrV43Y3c53RSLKOCJ5+Ow= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.1/go.mod h1:hPv41DbqMmnxcGralanA/kVlfdH5jv3T4LxGku2E1BY= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= -github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 h1:kYRSnvJju5gYVyhkij+RTJ/VR6QIUaCfWeaFm2ycsjQ= -github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= -github.com/Keyfactor/keyfactor-auth-client-go v1.2.0-rc.5 h1:3BpabiIcvdSszX7cx7OLBlpS8c7CnmvdquAANOD5+2o= -github.com/Keyfactor/keyfactor-auth-client-go v1.2.0-rc.5/go.mod h1:7htRcBIWn+X4fI5jaYBALSYwP84H/djN7d8y3n0ZDQ0= +github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/Keyfactor/keyfactor-auth-client-go v1.3.0 h1:otC213b6CYzqeN9b3CRlH1Qj1hTFIN5nqPA8gTlHdLg= +github.com/Keyfactor/keyfactor-auth-client-go v1.3.0/go.mod h1:97vCisBNkdCK0l2TuvOSdjlpvQa4+GHsMut1UTyv1jo= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -26,8 +26,8 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cu github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= -github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= -github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -36,8 +36,8 @@ github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB1 github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= -github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs= -github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw= +github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= +github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= @@ -58,8 +58,8 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmd github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/redis/go-redis/v9 v9.7.0 h1:HhLSs+B6O021gwzl+locl0zEDnyNkxMtf/Z3NNBMa9E= -github.com/redis/go-redis/v9 v9.7.0/go.mod h1:f6zhXITC7JUJIlPEiBOTXxJgPLdZcA93GewI7inzyWw= +github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI= +github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/spbsoluble/go-pkcs12 v0.3.3 h1:3nh7IKn16RDpmrSMtOu1JvbB0XHYq1j+IsICdU1c7J4= @@ -70,12 +70,12 @@ github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOf github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= go.mozilla.org/pkcs7 v0.9.0 h1:yM4/HS9dYv7ri2biPtxt8ikvB37a980dg69/pKmS+eI= go.mozilla.org/pkcs7 v0.9.0/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= -golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= -golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= -golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= -golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= -golang.org/x/oauth2 v0.25.0 h1:CY4y7XT9v0cRI9oupztF8AgiIu99L/ksR/Xp/6jrZ70= -golang.org/x/oauth2 v0.25.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -83,10 +83,10 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= -golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From 8dc5fd16f5559b083bcd5cf2b506b1c620fe03f3 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 23 Sep 2025 13:24:50 -0700 Subject: [PATCH 07/51] fix(certs): For V2 PFX enrollments don't require `subject` and update validation logic to require a `subject` or at least 1 `SAN` --- v3/api/certificate.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/v3/api/certificate.go b/v3/api/certificate.go index 95a7af8..8775a50 100644 --- a/v3/api/certificate.go +++ b/v3/api/certificate.go @@ -151,7 +151,11 @@ func (c *Client) EnrollPFXV2(ea *EnrollPFXFctArgsV2) (*EnrollResponseV2, error) } ea.SubjectString = subject } else { - return nil, fmt.Errorf("subject is required to use enrollpfx(). Please configure either SubjectString or Subject") + log.Println("[DEBUG] EnrollPFXV2: Subject is nil checks if there are SANs") + if ea.SANs == nil || (len(ea.SANs.DNS) == 0 && len(ea.SANs.URI) == 0 && len(ea.SANs.IP4) == 0 && + len(ea.SANs.IP6) == 0) { + return nil, fmt.Errorf("subject or subject alternative names are required to use enrollpfx(). Please configure either SubjectString or Subject or SANs") + } } } From de70b30f1a3d46e47b721a7afa2b6352c1386c11 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 23 Sep 2025 13:25:53 -0700 Subject: [PATCH 08/51] feat(certs): Add fields `AdditionalEnrollmentFields`, `AlternativeKeyType`, `AlternativeKeyLength` to `EnrollPFXFctArgsV2` --- v3/api/certificate_models.go | 38 ++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/v3/api/certificate_models.go b/v3/api/certificate_models.go index 98604f6..0f0e8e5 100644 --- a/v3/api/certificate_models.go +++ b/v3/api/certificate_models.go @@ -47,27 +47,35 @@ type EnrollPFXFctArgs struct { type EnrollPFXFctArgsV2 struct { Stores []CertificateStore `json:"Stores,omitempty"` CustomFriendlyName string `json:"CustomFriendlyName,omitempty"` - Password string `json:"Password"` + Password string `json:"Password,omitempty"` PopulateMissingValuesFromAD bool `json:"PopulateMissingValuesFromAD"` // Configure the SubjectString field as the full string subject for the certificate. For example, if you don't have // subject fields individually separated, and the subject is already in the format required by RFC5280, use the SubjectString field. - SubjectString string `json:"Subject"` // If the certificate subject is not already in the format required by RFC5280, configure the subject fields using a CertificateSubject // struct, and EnrollPFX will automatically compile this information into a proper subject. - Subject *CertificateSubject `json:"-"` - IncludeChain bool `json:"IncludeChain"` - RenewalCertificateId int `json:"RenewalCertificateId,omitempty"` - CertificateAuthority string `json:"CertificateAuthority"` - Timestamp string `json:"Timestamp"` - Template string `json:"Template"` - SANs *SANs `json:"SANs,omitempty"` - Metadata map[string]interface{} `json:"Metadata,omitempty"` - CertFormat string `json:"-"` - InstallIntoExistingCertificateStores bool `json:"InstallIntoExistingCertificateStores,omitempty"` - ChainOrder string `json:"ChainOrder,omitempty"` - KeyType string `json:"KeyType,omitempty"` - KeyLength int `json:"KeyLength,omitempty"` + Subject *CertificateSubject `json:"-"` + SubjectString string `json:"Subject,omitempty"` + IncludeChain bool `json:"IncludeChain"` + IncludeSubjectHeader bool `json:"IncludeSubjectHeader,omitempty"` + RenewalCertificateId int `json:"RenewalCertificateId,omitempty"` + CertificateAuthority string `json:"CertificateAuthority"` + Timestamp string `json:"Timestamp"` + Template string `json:"Template"` + SANs *SANs `json:"SANs,omitempty"` + Metadata map[string]interface{} `json:"Metadata,omitempty"` + AdditionalEnrollmentFields *map[string]interface{} `json:"AdditionalEnrollmentFields,omitempty"` + CertFormat string `json:"-"` // Needs to be passed as header X-Certificate-Format + InstallIntoExistingCertificateStores bool `json:"InstallIntoExistingCertificateStores,omitempty"` + ChainOrder string `json:"ChainOrder,omitempty"` + AlternativeKeyType string `json:"AlternativeKeyType,omitempty"` // Requires Command 25.0.0+ + KeyType string `json:"KeyType,omitempty"` + AlternativeKeyLength int `json:"AlternativeKeyLength,omitempty"` // Requires Command 25.0.0+ + KeyLength int `json:"KeyLength,omitempty"` + Curve string `json:"Curve,omitempty"` + EnrollmentPatternId int `json:"EnrollmentPatternId,omitempty"` // Requires Command 25.1.0+ + OwnerRoleId int `json:"OwnerRoleId,omitempty"` // Requires Command 12.3.0+ + OwnerRoleName string `json:"OwnerRoleName,omitempty"` // Requires Command 12.3.0+ } // EnrollCSRFctArgs holds the function arguments used for calling the EnrollCSR method. From 2612c4c37f3cf500d757a885a0b59192940a2886 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 23 Sep 2025 14:55:44 -0700 Subject: [PATCH 09/51] feat(enrollmentpatterns): Add `/EnrollmentPattern` models and endpoints --- v3/api/enrollment_patterns.go | 270 +++++++++++++++++++++++++++ v3/api/enrollment_patterns_models.go | 222 ++++++++++++++++++++++ 2 files changed, 492 insertions(+) create mode 100644 v3/api/enrollment_patterns.go create mode 100644 v3/api/enrollment_patterns_models.go diff --git a/v3/api/enrollment_patterns.go b/v3/api/enrollment_patterns.go new file mode 100644 index 0000000..574feb0 --- /dev/null +++ b/v3/api/enrollment_patterns.go @@ -0,0 +1,270 @@ +// Copyright 2024 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "encoding/json" + "errors" + "fmt" + "log" + "strconv" + "strings" +) + +// CreateEnrollmentPattern creates a new enrollment pattern with the provided properties +func (c *Client) CreateEnrollmentPattern( + req *EnrollmentPatternCreateRequest, + forceTemplateDefault ...bool, +) (*EnrollmentPatternResponse, error) { + log.Println("[INFO] Creating enrollment pattern with Keyfactor") + + // Validate required fields + var missingFields []string + if req.Name == "" { + missingFields = append(missingFields, "Name") + } + if req.Template == 0 { + missingFields = append(missingFields, "Template") + } + + if len(missingFields) > 0 { + return nil, errors.New("Required field(s) missing: " + strings.Join(missingFields, ", ")) + } + + // Set Keyfactor-specific headers + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + {"Content-Type", "application/json"}, + }, + } + + // Build URL with query parameters + endpoint := "EnrollmentPatterns" + if len(forceTemplateDefault) > 0 && forceTemplateDefault[0] { + endpoint += "?forceTemplateDefault=true" + } + + keyfactorAPIStruct := &request{ + Method: "POST", + Endpoint: endpoint, + Headers: headers, + Payload: req, + } + + resp, err := c.sendRequest(keyfactorAPIStruct) + if err != nil { + return nil, err + } + + jsonResp := &EnrollmentPatternResponse{} + err = json.NewDecoder(resp.Body).Decode(&jsonResp) + if err != nil { + return nil, err + } + + return jsonResp, nil +} + +// GetEnrollmentPatterns returns all enrollment patterns according to the provided filter and output parameters +func (c *Client) GetEnrollmentPatterns(params ...*EnrollmentPatternsQueryParams) ([]EnrollmentPatternResponse, error) { + log.Println("[INFO] Fetching enrollment patterns from Keyfactor") + + // Set Keyfactor-specific headers + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + }, + } + + // Build URL with query parameters + endpoint := "EnrollmentPatterns" + var queryParams []string + + if len(params) > 0 && params[0] != nil { + param := params[0] + if param.QueryString != "" { + queryParams = append(queryParams, "QueryString="+param.QueryString) + } + if param.PageReturned > 0 { + queryParams = append(queryParams, "PageReturned="+strconv.Itoa(param.PageReturned)) + } + if param.ReturnLimit > 0 { + queryParams = append(queryParams, "ReturnLimit="+strconv.Itoa(param.ReturnLimit)) + } + if param.SortField != "" { + queryParams = append(queryParams, "SortField="+param.SortField) + } + if param.SortAscending != nil { + queryParams = append(queryParams, "SortAscending="+strconv.Itoa(*param.SortAscending)) + } + } + + if len(queryParams) > 0 { + endpoint += "?" + strings.Join(queryParams, "&") + } + + keyfactorAPIStruct := &request{ + Method: "GET", + Endpoint: endpoint, + Headers: headers, + } + + resp, err := c.sendRequest(keyfactorAPIStruct) + if err != nil { + return nil, err + } + + var jsonResp []EnrollmentPatternResponse + err = json.NewDecoder(resp.Body).Decode(&jsonResp) + if err != nil { + return nil, err + } + + return jsonResp, nil +} + +// GetEnrollmentPattern returns the enrollment pattern associated with the provided ID +func (c *Client) GetEnrollmentPattern(id int) (*EnrollmentPatternResponse, error) { + log.Printf("[INFO] Fetching enrollment pattern with ID %d from Keyfactor", id) + + if id <= 0 { + return nil, errors.New("ID must be a positive integer") + } + + // Set Keyfactor-specific headers + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + }, + } + + keyfactorAPIStruct := &request{ + Method: "GET", + Endpoint: fmt.Sprintf("EnrollmentPatterns/%d", id), + Headers: headers, + } + + resp, err := c.sendRequest(keyfactorAPIStruct) + if err != nil { + return nil, err + } + + var jsonResp EnrollmentPatternResponse + err = json.NewDecoder(resp.Body).Decode(&jsonResp) + if err != nil { + return nil, err + } + + return &jsonResp, nil +} + +// UpdateEnrollmentPattern updates an enrollment pattern according to the provided properties and Keyfactor identifier +func (c *Client) UpdateEnrollmentPattern( + id int, + req *EnrollmentPatternRequest, + forceTemplateDefault ...bool, +) (*EnrollmentPatternResponse, error) { + log.Printf("[INFO] Updating enrollment pattern with ID %d in Keyfactor", id) + + if id <= 0 { + return nil, errors.New("ID must be a positive integer") + } + + // Validate required fields + var missingFields []string + if req.Name == "" { + missingFields = append(missingFields, "Name") + } + + if len(missingFields) > 0 { + return nil, errors.New("Required field(s) missing: " + strings.Join(missingFields, ", ")) + } + + // Set Keyfactor-specific headers + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + {"Content-Type", "application/json"}, + }, + } + + // Build URL with query parameters + endpoint := fmt.Sprintf("EnrollmentPatterns/%d", id) + if len(forceTemplateDefault) > 0 && forceTemplateDefault[0] { + endpoint += "?forceTemplateDefault=true" + } + + keyfactorAPIStruct := &request{ + Method: "PUT", + Endpoint: endpoint, + Headers: headers, + Payload: req, + } + + resp, err := c.sendRequest(keyfactorAPIStruct) + if err != nil { + return nil, err + } + + var jsonResp EnrollmentPatternResponse + err = json.NewDecoder(resp.Body).Decode(&jsonResp) + if err != nil { + return nil, err + } + + return &jsonResp, nil +} + +// DeleteEnrollmentPattern deletes an enrollment pattern by ID +// Note: This method assumes DELETE is supported based on REST conventions, +// though it may not be explicitly defined in the provided schema +func (c *Client) DeleteEnrollmentPattern(id int) error { + log.Printf("[INFO] Deleting enrollment pattern with ID %d from Keyfactor", id) + + if id <= 0 { + return errors.New("ID must be a positive integer") + } + + // Set Keyfactor-specific headers + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + }, + } + + keyfactorAPIStruct := &request{ + Method: "DELETE", + Endpoint: fmt.Sprintf("EnrollmentPatterns/%d", id), + Headers: headers, + } + + resp, err := c.sendRequest(keyfactorAPIStruct) + if err != nil { + return err + } + + // Check if the response indicates success (2xx status codes) + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("failed to delete enrollment pattern: HTTP %d", resp.StatusCode) + } + + return nil +} diff --git a/v3/api/enrollment_patterns_models.go b/v3/api/enrollment_patterns_models.go new file mode 100644 index 0000000..c8a2392 --- /dev/null +++ b/v3/api/enrollment_patterns_models.go @@ -0,0 +1,222 @@ +// Copyright 2024 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +// EnrollmentPatternCreateRequest represents the request structure for creating a new enrollment pattern +type EnrollmentPatternCreateRequest struct { + Template int `json:"Template"` + Name string `json:"Name"` + Description *string `json:"Description,omitempty"` + TemplateDefault bool `json:"TemplateDefault,omitempty"` + AssociatedRoles []string `json:"AssociatedRoles,omitempty"` + UseADPermissions bool `json:"UseADPermissions,omitempty"` + CertificateAuthorities []int `json:"CertificateAuthorities,omitempty"` + AllowedEnrollmentTypes int `json:"AllowedEnrollmentTypes,omitempty"` + Regexes []EnrollmentPatternRegexesRequest `json:"Regexes,omitempty"` + MetadataFields []EnrollmentPatternMetadataFieldRequest `json:"MetadataFields,omitempty"` + RestrictCAs bool `json:"RestrictCAs,omitempty"` + Policies EnrollmentPatternPolicyRequest `json:"Policies"` + Defaults []EnrollmentPatternDefaultRequest `json:"Defaults,omitempty"` + EnrollmentFields []EnrollmentPatternFieldRequest `json:"EnrollmentFields,omitempty"` +} + +// EnrollmentPatternRequest represents the request structure for updating an enrollment pattern +type EnrollmentPatternRequest struct { + Name string `json:"Name"` + Description *string `json:"Description,omitempty"` + TemplateDefault bool `json:"TemplateDefault,omitempty"` + AssociatedRoles []string `json:"AssociatedRoles,omitempty"` + UseADPermissions bool `json:"UseADPermissions,omitempty"` + CertificateAuthorities []int `json:"CertificateAuthorities,omitempty"` + AllowedEnrollmentTypes int `json:"AllowedEnrollmentTypes,omitempty"` + Regexes []EnrollmentPatternRegexesRequest `json:"Regexes,omitempty"` + MetadataFields []EnrollmentPatternMetadataFieldRequest `json:"MetadataFields,omitempty"` + RestrictCAs bool `json:"RestrictCAs,omitempty"` + Policies EnrollmentPatternPolicyRequest `json:"Policies"` + Defaults []EnrollmentPatternDefaultRequest `json:"Defaults,omitempty"` + EnrollmentFields []EnrollmentPatternFieldRequest `json:"EnrollmentFields,omitempty"` +} + +// EnrollmentPatternResponse represents the response structure for enrollment pattern operations +type EnrollmentPatternResponse struct { + ID int `json:"Id,omitempty"` + Name *string `json:"Name,omitempty"` + Description *string `json:"Description,omitempty"` + Template *EnrollmentPatternTemplateResponse `json:"Template,omitempty"` + TemplateDefault bool `json:"TemplateDefault,omitempty"` + UseADPermissions bool `json:"UseADPermissions,omitempty"` + AssociatedRoles []EnrollmentPatternAssociatedRoleResponse `json:"AssociatedRoles,omitempty"` + CertificateAuthorities []EnrollmentPatternCAResponse `json:"CertificateAuthorities,omitempty"` + AllowedEnrollmentTypes *int `json:"AllowedEnrollmentTypes,omitempty"` + Regexes []EnrollmentPatternRegexesResponse `json:"Regexes,omitempty"` + MetadataFields []EnrollmentPatternMetadataFieldResponse `json:"MetadataFields,omitempty"` + RestrictCAs bool `json:"RestrictCAs,omitempty"` + Policies *EnrollmentPatternPolicyResponse `json:"Policies,omitempty"` + Defaults []EnrollmentPatternDefaultResponse `json:"Defaults,omitempty"` + EnrollmentFields []EnrollmentPatternFieldResponse `json:"EnrollmentFields,omitempty"` +} + +// EnrollmentPatternRegexesRequest represents regex validation rules for enrollment patterns +type EnrollmentPatternRegexesRequest struct { + SubjectPart string `json:"SubjectPart"` + Regex string `json:"Regex,omitempty"` + Error string `json:"Error,omitempty"` + CaseSensitive bool `json:"CaseSensitive,omitempty"` +} + +// EnrollmentPatternRegexesResponse represents regex validation rules in responses +type EnrollmentPatternRegexesResponse struct { + SubjectPart *string `json:"SubjectPart,omitempty"` + Regex *string `json:"Regex,omitempty"` + Error *string `json:"Error,omitempty"` + CaseSensitive bool `json:"CaseSensitive,omitempty"` +} + +// EnrollmentPatternPolicyRequest represents policy settings for enrollment patterns +type EnrollmentPatternPolicyRequest struct { + AllowKeyReuse *bool `json:"AllowKeyReuse,omitempty"` + AllowWildcards *bool `json:"AllowWildcards,omitempty"` + RFCEnforcement *bool `json:"RFCEnforcement,omitempty"` + CertificateOwnerRole *int `json:"CertificateOwnerRole,omitempty"` + DefaultCertificateOwnerRoleId *int `json:"DefaultCertificateOwnerRoleId,omitempty"` + DefaultCertificateOwnerRoleName *string `json:"DefaultCertificateOwnerRoleName,omitempty"` + DefaultCertificateOwnerOverride bool `json:"DefaultCertificateOwnerOverride,omitempty"` + PrimaryKeyAlgorithms []AlgorithmDataRequestV2 `json:"PrimaryKeyAlgorithms,omitempty"` + AlternativeKeyAlgorithms []AlgorithmDataRequestV2 `json:"AlternativeKeyAlgorithms,omitempty"` +} + +// EnrollmentPatternPolicyResponse represents policy settings in responses +type EnrollmentPatternPolicyResponse struct { + AllowKeyReuse *bool `json:"AllowKeyReuse,omitempty"` + AllowWildcards *bool `json:"AllowWildcards,omitempty"` + RFCEnforcement *bool `json:"RFCEnforcement,omitempty"` + CertificateOwnerRole *int `json:"CertificateOwnerRole,omitempty"` + DefaultCertificateOwnerRoleId *int `json:"DefaultCertificateOwnerRoleId,omitempty"` + DefaultCertificateOwnerRoleName *string `json:"DefaultCertificateOwnerRoleName,omitempty"` + DefaultCertificateOwnerOverride bool `json:"DefaultCertificateOwnerOverride,omitempty"` + PrimaryKeyAlgorithms []AlgorithmDataResponse `json:"PrimaryKeyAlgorithms,omitempty"` + AlternativeKeyAlgorithms []AlgorithmDataResponse `json:"AlternativeKeyAlgorithms,omitempty"` +} + +// EnrollmentPatternMetadataFieldRequest represents metadata field configuration for requests +type EnrollmentPatternMetadataFieldRequest struct { + Id int `json:"Id,omitempty"` + DefaultValue string `json:"DefaultValue,omitempty"` + Validation string `json:"Validation,omitempty"` + Enrollment int `json:"Enrollment,omitempty"` + Message string `json:"Message,omitempty"` + Options string `json:"Options,omitempty"` + DependsOn string `json:"DependsOn,omitempty"` + DependsOnValue string `json:"DependsOnValue,omitempty"` +} + +// EnrollmentPatternMetadataFieldResponse represents metadata field configuration in responses +type EnrollmentPatternMetadataFieldResponse struct { + Id *int `json:"Id,omitempty"` + Name *string `json:"Name,omitempty"` + DefaultValue *string `json:"DefaultValue,omitempty"` + Validation *string `json:"Validation,omitempty"` + Enrollment *int `json:"Enrollment,omitempty"` + Message *string `json:"Message,omitempty"` + Options *string `json:"Options,omitempty"` + DependsOn *string `json:"DependsOn,omitempty"` + DependsOnValue *string `json:"DependsOnValue,omitempty"` + DataType *int `json:"DataType,omitempty"` + Hint *string `json:"Hint,omitempty"` +} + +// EnrollmentPatternDefaultRequest represents default value settings for requests +type EnrollmentPatternDefaultRequest struct { + SubjectPart string `json:"SubjectPart"` + DefaultValue string `json:"DefaultValue,omitempty"` +} + +// EnrollmentPatternDefaultResponse represents default value settings in responses +type EnrollmentPatternDefaultResponse struct { + SubjectPart *string `json:"SubjectPart,omitempty"` + DefaultValue *string `json:"DefaultValue,omitempty"` +} + +// EnrollmentPatternFieldRequest represents enrollment field configuration for requests +type EnrollmentPatternFieldRequest struct { + Id int `json:"Id,omitempty"` + DefaultValue string `json:"DefaultValue,omitempty"` + Validation string `json:"Validation,omitempty"` + Enrollment int `json:"Enrollment,omitempty"` + Message string `json:"Message,omitempty"` + Options string `json:"Options,omitempty"` + DependsOn string `json:"DependsOn,omitempty"` + DependsOnValue string `json:"DependsOnValue,omitempty"` +} + +// EnrollmentPatternFieldResponse represents enrollment field configuration in responses +type EnrollmentPatternFieldResponse struct { + Id *int `json:"Id,omitempty"` + Name *string `json:"Name,omitempty"` + DefaultValue *string `json:"DefaultValue,omitempty"` + Validation *string `json:"Validation,omitempty"` + Enrollment *int `json:"Enrollment,omitempty"` + Message *string `json:"Message,omitempty"` + Options *string `json:"Options,omitempty"` + DependsOn *string `json:"DependsOn,omitempty"` + DependsOnValue *string `json:"DependsOnValue,omitempty"` + DataType *int `json:"DataType,omitempty"` + Hint *string `json:"Hint,omitempty"` +} + +// EnrollmentPatternTemplateResponse represents template information in responses +type EnrollmentPatternTemplateResponse struct { + Id *int `json:"Id,omitempty"` + Name *string `json:"Name,omitempty"` + CommonName *string `json:"CommonName,omitempty"` + DisplayName *string `json:"DisplayName,omitempty"` +} + +// EnrollmentPatternAssociatedRoleResponse represents associated role information in responses +type EnrollmentPatternAssociatedRoleResponse struct { + Id *int `json:"Id,omitempty"` + Name *string `json:"Name,omitempty"` +} + +// EnrollmentPatternCAResponse represents certificate authority information in responses +type EnrollmentPatternCAResponse struct { + Id *int `json:"Id,omitempty"` + LogicalName *string `json:"LogicalName,omitempty"` + HostName *string `json:"HostName,omitempty"` + CAName *string `json:"CAName,omitempty"` +} + +// AlgorithmDataRequestV2 represents algorithm configuration for requests +type AlgorithmDataRequestV2 struct { + KeyType *string `json:"KeyType,omitempty"` + KeySize *int `json:"KeySize,omitempty"` + CurveName *string `json:"CurveName,omitempty"` +} + +// AlgorithmDataResponse represents algorithm configuration in responses +type AlgorithmDataResponse struct { + KeyType *string `json:"KeyType,omitempty"` + KeySize *int `json:"KeySize,omitempty"` + CurveName *string `json:"CurveName,omitempty"` +} + +// EnrollmentPatternsQueryParams represents query parameters for listing enrollment patterns +type EnrollmentPatternsQueryParams struct { + QueryString string `json:"queryString,omitempty"` + PageReturned int `json:"pageReturned,omitempty"` + ReturnLimit int `json:"returnLimit,omitempty"` + SortField string `json:"sortField,omitempty"` + SortAscending *int `json:"sortAscending,omitempty"` // 0=ascending, 1=descending +} From b1091890eebe5d90de61f3ee51a4eb8f654e4a77 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 24 Sep 2025 09:47:57 -0700 Subject: [PATCH 10/51] feat(certficates): Add `OwnerRoleId,OwnerRoleName,AltKeyAlgorithm,AltKeySizeInBits,AltKeyType,IssuedEmail,AltSigningAlgorithm,AltKeyTypeString,HasAltPrivateKey,CARecordId,Curve,EnrollmentPatternId` to `GetCertificateResponse` model --- v3/api/certificate_models.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/v3/api/certificate_models.go b/v3/api/certificate_models.go index 0f0e8e5..0dd8021 100644 --- a/v3/api/certificate_models.go +++ b/v3/api/certificate_models.go @@ -221,16 +221,25 @@ type GetCertificateResponse struct { NotAfter string `json:"NotAfter"` IssuerDN string `json:"IssuerDN"` PrincipalId string `json:"PrincipalId"` + OwnerRoleId int `json:"OwnerRoleId;omitempty"` // Requires Command 12.3.0+ + OwnerRoleName string `json:"OwnerRoleName,omitempty"` // Requires Command 12.3.0+ TemplateId int `json:"TemplateId"` CertState int `json:"CertState"` KeySizeInBits int `json:"KeySizeInBits"` KeyType int `json:"KeyType"` + KeyAlgorithm string `json:"KeyAlgorithm"` + AltKeyAlgorithm string `json:"AltKeyAlgorithm,omitempty"` // Requires Command 25.0.0+ + AltKeySizeInBits int `json:"AltKeySizeInBits,omitempty"` // Requires Command 25.0.0+ + AltKeyType int `json:"AltKeyType,omitempty"` // Requires Command 25.0.0+ RequesterId int `json:"RequesterId"` IssuedOU string `json:"IssuedOU"` + IssuedEmail string `json:"IssuedEmail"` KeyUsage int `json:"KeyUsage"` SigningAlgorithm string `json:"SigningAlgorithm"` + AltSigningAlgorithm string `json:"AltSigningAlgorithm,omitempty"` // Requires Command 25.0.0+ CertStateString string `json:"CertStateString"` KeyTypeString string `json:"KeyTypeString"` + AltKeyTypeString string `json:"AltKeyTypeString,omitempty"` // Requires Command 25.0.0+ RevocationEffDate string `json:"RevocationEffDate"` RevocationReason int `json:"RevocationReason"` RevocationComment string `json:"RevocationComment"` @@ -239,6 +248,7 @@ type GetCertificateResponse struct { TemplateName string `json:"TemplateName"` ArchivedKey bool `json:"ArchivedKey"` HasPrivateKey bool `json:"HasPrivateKey"` + HasAltPrivateKey bool `json:"HasAltPrivateKey,omitempty"` // Requires Command 25.0.0+ PrincipalName string `json:"PrincipalName"` CertRequestId int `json:"CertRequestId"` RequesterName string `json:"RequesterName"` @@ -252,8 +262,11 @@ type GetCertificateResponse struct { Metadata interface{} `json:"Metadata"` CertificateKeyId int `json:"CertificateKeyId"` CARowIndex int `json:"CARowIndex"` + CARecordId string `json:"CARecordId"` DetailedKeyUsage []DetailedKeyUsage `json:"detailed_key_usage"` KeyRecoverable bool `json:"KeyRecoverable"` + Curve string `json:"Curve,omitempty"` + EnrollmentPatternId int `json:"EnrollmentPatternId,omitempty"` // Requires Command 25.1.0+ } type ListCertificateResponse struct { From 5175bb8d3dfe554c5db9f15960cd2a4a10b7321a Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 24 Sep 2025 09:48:57 -0700 Subject: [PATCH 11/51] fix(stores): Change `Password` type from `UpdateStorePasswordConfig` to `StorePasswordConfig` on `UpdateStoreFctArgs` --- v3/api/store_models.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/v3/api/store_models.go b/v3/api/store_models.go index bf4db2c..a210cad 100644 --- a/v3/api/store_models.go +++ b/v3/api/store_models.go @@ -51,14 +51,14 @@ type UpdateStoreFctArgs struct { // automatically populated by the CreateStore method. However, if configured, this field will be used. PropertiesString string `json:"Properties,omitempty"` // Mapped name-value pair field used to configure properties. - Properties map[string]interface{} `json:"-"` - AgentId string `json:"AgentId"` - AgentAssigned *bool `json:"AgentAssigned,omitempty"` - ContainerName *string `json:"ContainerName,omitempty"` - InventorySchedule *InventorySchedule `json:"InventorySchedule,omitempty"` - ReEnrollmentStatus *ReEnrollmnentConfig `json:"ReEnrollmentStatus,omitempty"` - SetNewPasswordAllowed *bool `json:"SetNewPasswordAllowed,omitempty"` - Password *UpdateStorePasswordConfig `json:"Password"` + Properties map[string]interface{} `json:"-"` + AgentId string `json:"AgentId"` + AgentAssigned *bool `json:"AgentAssigned,omitempty"` + ContainerName *string `json:"ContainerName,omitempty"` + InventorySchedule *InventorySchedule `json:"InventorySchedule,omitempty"` + ReEnrollmentStatus *ReEnrollmnentConfig `json:"ReEnrollmentStatus,omitempty"` + SetNewPasswordAllowed *bool `json:"SetNewPasswordAllowed,omitempty"` + Password *StorePasswordConfig `json:"Password"` } type UpdateStorePasswordConfig struct { From bb177bc5c58f3aade6c97b23babdb81b1ce6cf8c Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 24 Sep 2025 12:04:49 -0700 Subject: [PATCH 12/51] fix(certs): Check that `Template` and `EnrollmentPattern` are not both empty for enrollments --- v3/api/certificate.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/v3/api/certificate.go b/v3/api/certificate.go index 8775a50..eccb9e6 100644 --- a/v3/api/certificate.go +++ b/v3/api/certificate.go @@ -113,8 +113,8 @@ func (c *Client) EnrollPFXV2(ea *EnrollPFXFctArgsV2) (*EnrollResponseV2, error) var missingFields []string // TODO: Probably a better way to express these if blocks - if ea.Template == "" { - missingFields = append(missingFields, "Template") + if ea.Template == "" && ea.EnrollmentPatternId == 0 { + missingFields = append(missingFields, "Template or EnrollmentPatternId") } if ea.CertificateAuthority == "" { missingFields = append(missingFields, "CertificateAuthority") From 02ac064269c83b5e1f0ac4c0decf85f1f552629f Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 24 Sep 2025 13:24:09 -0700 Subject: [PATCH 13/51] fix(certs): Add `ChangeCertificateOwnerRole` --- v3/api/certificate.go | 62 ++++++++++++++++++++++++++++++++++++ v3/api/certificate_models.go | 12 +++++++ 2 files changed, 74 insertions(+) diff --git a/v3/api/certificate.go b/v3/api/certificate.go index eccb9e6..a73794b 100644 --- a/v3/api/certificate.go +++ b/v3/api/certificate.go @@ -768,6 +768,68 @@ func (c *Client) RecoverCertificate( return priv, leaf, chain, nil } +// ChangeCertificateOwnerRole changes the certificate's owner. Users must be in the current owner's role and the new owner's role. +// If removing the owner, leave both NewRoleId and NewRoleName empty in the request. +// Calls PUT /Certificates/{id}/Owner endpoint. +func (c *Client) ChangeCertificateOwnerRole( + certificateId int, + req *OwnerRequest, + params ...*CertificateOwnerChangeParams, +) error { + log.Printf("[INFO] Changing owner of certificate with ID %d in Keyfactor", certificateId) + + // Validate certificate ID + if certificateId <= 0 { + return errors.New("certificate ID must be a positive integer") + } + + // Set Keyfactor-specific headers + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + {"Content-Type", "application/json"}, + }, + } + + // Build URL with query parameters + endpoint := fmt.Sprintf("Certificates/%d/Owner", certificateId) + var queryParams []string + + if len(params) > 0 && params[0] != nil { + param := params[0] + if param.CollectionId != nil { + queryParams = append(queryParams, fmt.Sprintf("collectionId=%d", *param.CollectionId)) + } + if param.ContainerId != nil { + queryParams = append(queryParams, fmt.Sprintf("containerId=%d", *param.ContainerId)) + } + } + + if len(queryParams) > 0 { + endpoint += "?" + strings.Join(queryParams, "&") + } + + keyfactorAPIStruct := &request{ + Method: "PUT", + Endpoint: endpoint, + Headers: headers, + Payload: req, + } + + resp, err := c.sendRequest(keyfactorAPIStruct) + if err != nil { + return err + } + + // Check if the response indicates success (204 No Content expected) + if resp.StatusCode != http.StatusNoContent { + return fmt.Errorf("failed to change certificate owner: HTTP %d", resp.StatusCode) + } + + return nil +} + // createSubject builds the certificate subject string from a passed CertificateSubject argument. func createSubject(cs CertificateSubject) (string, error) { var subject string diff --git a/v3/api/certificate_models.go b/v3/api/certificate_models.go index 0dd8021..e1c2b8b 100644 --- a/v3/api/certificate_models.go +++ b/v3/api/certificate_models.go @@ -340,3 +340,15 @@ type SubjectAltNameElements struct { type downloadCertificateResponse struct { Content string `json:"Content"` } + +// OwnerRequest represents the request structure for changing certificate ownership +type OwnerRequest struct { + NewRoleId *int `json:"NewRoleId,omitempty"` + NewRoleName *string `json:"NewRoleName,omitempty"` +} + +// CertificateOwnerChangeParams represents the parameters for changing certificate ownership +type CertificateOwnerChangeParams struct { + CollectionId *int `json:"collectionId,omitempty"` + ContainerId *int `json:"containerId,omitempty"` +} From c2ae7d3cca05b83cb15394e498ff602c4126e8b7 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 25 Sep 2025 11:21:08 -0700 Subject: [PATCH 14/51] feat(certs): Add the following fields to `EnrollCSRFctArgs`: `PrivateKey,RenewalCertificateId,AdditionalEnrollmentFields,EnrollmentPatternId,OwnerRoleId,OwnerRoleName,IncludeSubjectHeader` --- v3/api/certificate_models.go | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/v3/api/certificate_models.go b/v3/api/certificate_models.go index e1c2b8b..945ab65 100644 --- a/v3/api/certificate_models.go +++ b/v3/api/certificate_models.go @@ -80,14 +80,21 @@ type EnrollPFXFctArgsV2 struct { // EnrollCSRFctArgs holds the function arguments used for calling the EnrollCSR method. type EnrollCSRFctArgs struct { - CSR string - Timestamp string `json:"Timestamp"` - Template string `json:"Template"` - CertFormat string `json:"-"` - CertificateAuthority string `json:"CertificateAuthority"` - IncludeChain bool `json:"IncludeChain"` - SANs *SANs `json:"SANs"` - Metadata map[string]interface{} `json:"Metadata"` + CSR string `json:"CSR"` //required + PrivateKey string `json:"PrivateKey,omitempty"` + RenewalCertificateId int `json:"RenewalCertificateId,omitempty"` + CertificateAuthority string `json:"CertificateAuthority,omitempty"` + IncludeChain bool `json:"IncludeChain"` + IncludeSubjectHeader bool `json:"IncludeSubjectHeader,omitempty"` + Timestamp string `json:"Timestamp"` + Template string `json:"Template,omitempty"` + EnrollmentPatternId int `json:"EnrollmentPatternId,omitempty"` // Requires Command 25.1.0+ + CertFormat string `json:"-"` + SANs *SANs `json:"SANs,omitempty"` + Metadata map[string]interface{} `json:"Metadata,omitempty"` + AdditionalEnrollmentFields map[string]interface{} `json:"AdditionalEnrollmentFields,omitempty"` + OwnerRoleId int `json:"OwnerRoleId,omitempty"` // Requires Command 12.3.0+ + OwnerRoleName string `json:"OwnerRoleName,omitempty"` // Requires Command 12.3.0+ } // RevokeCertArgs holds the function arguments used for calling the RevokeCert method. From f82208c941177c623d56a3edfdd2dc4c635d8608 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Sun, 28 Sep 2025 13:25:20 -0700 Subject: [PATCH 15/51] feat(certs): Add base64 response from `DownloadCertificate` --- v3/api/certificate.go | 127 ++++++++++++++++++++++++++++++++---------- 1 file changed, 99 insertions(+), 28 deletions(-) diff --git a/v3/api/certificate.go b/v3/api/certificate.go index a73794b..bd01c40 100644 --- a/v3/api/certificate.go +++ b/v3/api/certificate.go @@ -195,13 +195,16 @@ func (c *Client) EnrollPFXV2(ea *EnrollPFXFctArgsV2) (*EnrollResponseV2, error) // Returns: // - Leaf certificate // - Certificate chain +// - Raw certificate data (as base64 string, if applicable) +// - Error func (c *Client) DownloadCertificate( certId int, thumbprint string, serialNumber string, issuerDn string, collectionId int, -) (*x509.Certificate, []*x509.Certificate, error) { + certificateFormat string, +) (*x509.Certificate, []*x509.Certificate, *string, error) { log.Println("[INFO] Downloading certificate") /* The download certificate endpoint requires one of the following to retrieve a cert: @@ -221,7 +224,7 @@ func (c *Client) DownloadCertificate( } if !validInput { - return nil, nil, fmt.Errorf("certID, thumbprint, or serial number AND issuer DN required to dowload certificate") + return nil, nil, nil, fmt.Errorf("certID, thumbprint, or serial number AND issuer DN required to dowload certificate") } payload := &downloadCertificateBody{ @@ -247,11 +250,19 @@ func (c *Client) DownloadCertificate( } // Set Keyfactor-specific headers + switch certificateFormat { + case "CER", "CRT", "DER", "PEM": + // do nothing these are valid formats + break + default: + // if not specified or invalid format then default to P7B + certificateFormat = "P7B" + } headers := &apiHeaders{ Headers: []StringTuple{ {"x-keyfactor-api-version", "1"}, {"x-keyfactor-requested-with", "APIClient"}, - {"x-certificateformat", "P7B"}, + {"x-certificateformat", certificateFormat}, }, } @@ -265,13 +276,13 @@ func (c *Client) DownloadCertificate( resp, err := c.sendRequest(keyfactorAPIStruct) if err != nil { - return nil, nil, err + return nil, nil, nil, err } jsonResp := &downloadCertificateResponse{} err = json.NewDecoder(resp.Body).Decode(&jsonResp) if err != nil { - return nil, nil, err + return nil, nil, nil, err } //buf, err := base64.StdEncoding.DecodeString(jsonResp.Content) //if err != nil { @@ -285,17 +296,17 @@ func (c *Client) DownloadCertificate( certs, p7bErr := ConvertBase64P7BtoCertificates(jsonResp.Content) if p7bErr != nil { - return nil, nil, p7bErr + return nil, nil, &jsonResp.Content, p7bErr } var leaf *x509.Certificate if len(certs) > 1 { //leaf is last cert in chain leaf = certs[0] // First cert in chain is the leaf - return leaf, certs, nil + return leaf, certs, &jsonResp.Content, nil } - return certs[0], nil, nil + return certs[0], nil, &jsonResp.Content, nil } // EnrollCSR takes arguments for EnrollCSRFctArgs to enroll a passed Certificate Signing @@ -663,7 +674,8 @@ func (c *Client) RecoverCertificate( issuerDn string, password string, collectionId int, -) (interface{}, *x509.Certificate, []*x509.Certificate, error) { + certificateFormat string, +) (interface{}, *x509.Certificate, []*x509.Certificate, *string, error) { log.Println("[DEBUG] Enter RecoverCertificate") log.Println("[INFO] Recovering certificate ID:", certId) /* The download certificate endpoint requires one of the following to retrieve a cert: @@ -673,6 +685,9 @@ func (c *Client) RecoverCertificate( Check for this input */ + if certificateFormat == "" { + certificateFormat = "PFX" + } validInput := false if certId != 0 { validInput = true @@ -684,12 +699,12 @@ func (c *Client) RecoverCertificate( if !validInput { log.Println("[ERROR] RecoverCertificate: certID, thumbprint, or serial number AND issuer DN required to download certificate") - return nil, nil, nil, fmt.Errorf("certID, thumbprint, or serial number AND issuer DN required to download certificate") + return nil, nil, nil, nil, fmt.Errorf("certID, thumbprint, or serial number AND issuer DN required to download certificate") } log.Println("[DEBUG] RecoverCertificate: Valid input") if password == "" { - return nil, nil, nil, fmt.Errorf("password required to recover private key with certificate") + return nil, nil, nil, nil, fmt.Errorf("password required to recover private key with certificate") } rca := &recoverCertArgs{ @@ -707,7 +722,7 @@ func (c *Client) RecoverCertificate( Headers: []StringTuple{ {"x-keyfactor-api-version", "1"}, {"x-keyfactor-requested-with", "APIClient"}, - {"x-certificateformat", "PFX"}, + {"x-certificateformat", certificateFormat}, }, } @@ -738,7 +753,7 @@ func (c *Client) RecoverCertificate( resp, err := c.sendRequest(keyfactorAPIStruct) if err != nil { log.Println("[ERROR] RecoverCertificate: Error recovering certificate from Keyfactor Command", err.Error()) - return nil, nil, nil, err + return nil, nil, nil, nil, err } jsonResp := &recoverCertResponse{} @@ -746,26 +761,82 @@ func (c *Client) RecoverCertificate( err = json.NewDecoder(resp.Body).Decode(&jsonResp) if err != nil { log.Println("[ERROR] RecoverCertificate: Error decoding response from Keyfactor Command", err.Error()) - return nil, nil, nil, err + return nil, nil, nil, nil, err } - log.Println("[DEBUG] RecoverCertificate: Decoding PFX") - pfxDer, err := base64.StdEncoding.DecodeString(jsonResp.PFX) - if err != nil { - log.Println("[ERROR] RecoverCertificate: Error decoding PFX", err.Error()) - return nil, nil, nil, err - } + switch certificateFormat { + case "PFX", "pfx", "pkcs12", "p12", "jks", "JKS": + log.Println("[DEBUG] RecoverCertificate: decoding `PFX` response field") + pfxDer := jsonResp.PFX + if pfxDer == "" { + log.Println("[ERROR] RecoverCertificate: Error decoding PFX", err.Error()) + return nil, nil, nil, &pfxDer, fmt.Errorf("pfx field in response is empty") + } + log.Println("[INFO] Recovered certificate successfully") + log.Println("[DEBUG] RecoverCertificate returning in PFX format") + return nil, nil, nil, &pfxDer, nil + case "PEM", "pem": + log.Println("[DEBUG] RecoverCertificate: Decoding PFX") + pfxDer, dErr := base64.StdEncoding.DecodeString(jsonResp.PFX) + if dErr != nil { + log.Println("[ERROR] RecoverCertificate: Error decoding PFX", dErr.Error()) + return nil, nil, nil, &jsonResp.PFX, dErr + } - log.Println("[DEBUG] RecoverCertificate: Decoding PFX chain") - priv, leaf, chain, err := pkcs12.DecodeChain(pfxDer, rca.Password) - if err != nil { - log.Println("[ERROR] RecoverCertificate: Error decoding PFX chain", err.Error()) - return nil, nil, nil, err + log.Println("[DEBUG] RecoverCertificate: Decoding PFX chain") + priv, leaf, chain, pErr := pkcs12.DecodeChain(pfxDer, rca.Password) + if pErr != nil { + log.Println("[ERROR] RecoverCertificate: Error decoding PFX chain", pErr.Error()) + return nil, nil, nil, &jsonResp.PFX, pErr + } + + log.Println("[INFO] Recovered certificate successfully") + log.Println("[DEBUG] RecoverCertificate: ", leaf, chain) + return priv, leaf, chain, &jsonResp.PFX, nil + default: + log.Println("[DEBUG] RecoverCertificate: Decoding PFX") + pfxDer, dErr := base64.StdEncoding.DecodeString(jsonResp.PFX) + if dErr != nil { + log.Println("[ERROR] RecoverCertificate: Error decoding PFX", dErr.Error()) + return nil, nil, nil, &jsonResp.PFX, dErr + } + + log.Println("[DEBUG] RecoverCertificate: Decoding PFX chain") + priv, leaf, chain, pErr := pkcs12.DecodeChain(pfxDer, rca.Password) + if pErr != nil { + log.Println("[ERROR] RecoverCertificate: Error decoding PFX chain", pErr.Error()) + return nil, nil, nil, &jsonResp.PFX, pErr + } + + log.Println("[INFO] Recovered certificate successfully") + log.Println("[DEBUG] RecoverCertificate returning in PEM format") + + var pemCerts []string + + // Encode leaf certificate to PEM + pemLeaf := pem.EncodeToMemory( + &pem.Block{ + Type: "CERTIFICATE", + Bytes: leaf.Raw, + }, + ) + pemCerts = append(pemCerts, string(pemLeaf)) + + // Encode chain certificates to PEM + for _, cert := range chain { + pemCert := pem.EncodeToMemory( + &pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Raw, + }, + ) + pemCerts = append(pemCerts, string(pemCert)) + } + + pemData := strings.Join(pemCerts, "\n") + return priv, leaf, chain, &pemData, nil } - log.Println("[INFO] Recovered certificate successfully") - log.Println("[DEBUG] RecoverCertificate: ", leaf, chain) - return priv, leaf, chain, nil } // ChangeCertificateOwnerRole changes the certificate's owner. Users must be in the current owner's role and the new owner's role. From 6868250fcac81776bcfb6b495f11a22ef25d2686 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 1 Oct 2025 11:00:48 -0700 Subject: [PATCH 16/51] fix(patterns): Fix enrollmentpattern models --- v3/api/enrollment_patterns_models.go | 97 ++++++++++++++-------------- 1 file changed, 47 insertions(+), 50 deletions(-) diff --git a/v3/api/enrollment_patterns_models.go b/v3/api/enrollment_patterns_models.go index c8a2392..0d2ac42 100644 --- a/v3/api/enrollment_patterns_models.go +++ b/v3/api/enrollment_patterns_models.go @@ -35,7 +35,7 @@ type EnrollmentPatternCreateRequest struct { // EnrollmentPatternRequest represents the request structure for updating an enrollment pattern type EnrollmentPatternRequest struct { Name string `json:"Name"` - Description *string `json:"Description,omitempty"` + Description string `json:"Description,omitempty"` TemplateDefault bool `json:"TemplateDefault,omitempty"` AssociatedRoles []string `json:"AssociatedRoles,omitempty"` UseADPermissions bool `json:"UseADPermissions,omitempty"` @@ -52,8 +52,8 @@ type EnrollmentPatternRequest struct { // EnrollmentPatternResponse represents the response structure for enrollment pattern operations type EnrollmentPatternResponse struct { ID int `json:"Id,omitempty"` - Name *string `json:"Name,omitempty"` - Description *string `json:"Description,omitempty"` + Name string `json:"Name,omitempty"` + Description string `json:"Description,omitempty"` Template *EnrollmentPatternTemplateResponse `json:"Template,omitempty"` TemplateDefault bool `json:"TemplateDefault,omitempty"` UseADPermissions bool `json:"UseADPermissions,omitempty"` @@ -78,10 +78,10 @@ type EnrollmentPatternRegexesRequest struct { // EnrollmentPatternRegexesResponse represents regex validation rules in responses type EnrollmentPatternRegexesResponse struct { - SubjectPart *string `json:"SubjectPart,omitempty"` - Regex *string `json:"Regex,omitempty"` - Error *string `json:"Error,omitempty"` - CaseSensitive bool `json:"CaseSensitive,omitempty"` + SubjectPart string `json:"SubjectPart,omitempty"` + Regex string `json:"Regex,omitempty"` + Error string `json:"Error,omitempty"` + CaseSensitive bool `json:"CaseSensitive,omitempty"` } // EnrollmentPatternPolicyRequest represents policy settings for enrollment patterns @@ -99,12 +99,12 @@ type EnrollmentPatternPolicyRequest struct { // EnrollmentPatternPolicyResponse represents policy settings in responses type EnrollmentPatternPolicyResponse struct { - AllowKeyReuse *bool `json:"AllowKeyReuse,omitempty"` - AllowWildcards *bool `json:"AllowWildcards,omitempty"` - RFCEnforcement *bool `json:"RFCEnforcement,omitempty"` - CertificateOwnerRole *int `json:"CertificateOwnerRole,omitempty"` - DefaultCertificateOwnerRoleId *int `json:"DefaultCertificateOwnerRoleId,omitempty"` - DefaultCertificateOwnerRoleName *string `json:"DefaultCertificateOwnerRoleName,omitempty"` + AllowKeyReuse bool `json:"AllowKeyReuse,omitempty"` + AllowWildcards bool `json:"AllowWildcards,omitempty"` + RFCEnforcement bool `json:"RFCEnforcement,omitempty"` + CertificateOwnerRole int `json:"CertificateOwnerRole,omitempty"` + DefaultCertificateOwnerRoleId int `json:"DefaultCertificateOwnerRoleId,omitempty"` + DefaultCertificateOwnerRoleName string `json:"DefaultCertificateOwnerRoleName,omitempty"` DefaultCertificateOwnerOverride bool `json:"DefaultCertificateOwnerOverride,omitempty"` PrimaryKeyAlgorithms []AlgorithmDataResponse `json:"PrimaryKeyAlgorithms,omitempty"` AlternativeKeyAlgorithms []AlgorithmDataResponse `json:"AlternativeKeyAlgorithms,omitempty"` @@ -124,17 +124,12 @@ type EnrollmentPatternMetadataFieldRequest struct { // EnrollmentPatternMetadataFieldResponse represents metadata field configuration in responses type EnrollmentPatternMetadataFieldResponse struct { - Id *int `json:"Id,omitempty"` - Name *string `json:"Name,omitempty"` - DefaultValue *string `json:"DefaultValue,omitempty"` - Validation *string `json:"Validation,omitempty"` - Enrollment *int `json:"Enrollment,omitempty"` - Message *string `json:"Message,omitempty"` - Options *string `json:"Options,omitempty"` - DependsOn *string `json:"DependsOn,omitempty"` - DependsOnValue *string `json:"DependsOnValue,omitempty"` - DataType *int `json:"DataType,omitempty"` - Hint *string `json:"Hint,omitempty"` + MetadataId int `json:"MetadataId,omitempty"` + DefaultValue string `json:"DefaultValue,omitempty"` + Validation string `json:"Validation,omitempty"` + Enrollment int `json:"Enrollment,omitempty"` + Message string `json:"Message,omitempty"` + CaseSensitive bool `json:"CaseSensitive,omitempty"` } // EnrollmentPatternDefaultRequest represents default value settings for requests @@ -145,8 +140,8 @@ type EnrollmentPatternDefaultRequest struct { // EnrollmentPatternDefaultResponse represents default value settings in responses type EnrollmentPatternDefaultResponse struct { - SubjectPart *string `json:"SubjectPart,omitempty"` - DefaultValue *string `json:"DefaultValue,omitempty"` + SubjectPart string `json:"SubjectPart,omitempty"` + Value string `json:"Value,omitempty"` } // EnrollmentPatternFieldRequest represents enrollment field configuration for requests @@ -163,39 +158,41 @@ type EnrollmentPatternFieldRequest struct { // EnrollmentPatternFieldResponse represents enrollment field configuration in responses type EnrollmentPatternFieldResponse struct { - Id *int `json:"Id,omitempty"` - Name *string `json:"Name,omitempty"` - DefaultValue *string `json:"DefaultValue,omitempty"` - Validation *string `json:"Validation,omitempty"` - Enrollment *int `json:"Enrollment,omitempty"` - Message *string `json:"Message,omitempty"` - Options *string `json:"Options,omitempty"` - DependsOn *string `json:"DependsOn,omitempty"` - DependsOnValue *string `json:"DependsOnValue,omitempty"` - DataType *int `json:"DataType,omitempty"` - Hint *string `json:"Hint,omitempty"` + Id int `json:"Id,omitempty"` + Name string `json:"Name,omitempty"` + DefaultValue string `json:"DefaultValue,omitempty"` + Validation string `json:"Validation,omitempty"` + Enrollment int `json:"Enrollment,omitempty"` + Message string `json:"Message,omitempty"` + Options []string `json:"Options,omitempty"` + DependsOn string `json:"DependsOn,omitempty"` + DependsOnValue string `json:"DependsOnValue,omitempty"` + DataType int `json:"DataType,omitempty"` + Hint string `json:"Hint,omitempty"` } // EnrollmentPatternTemplateResponse represents template information in responses type EnrollmentPatternTemplateResponse struct { - Id *int `json:"Id,omitempty"` - Name *string `json:"Name,omitempty"` - CommonName *string `json:"CommonName,omitempty"` - DisplayName *string `json:"DisplayName,omitempty"` + Id int `json:"Id,omitempty"` + TemplateName string `json:"TemplateName,omitempty"` + CommonName string `json:"CommonName,omitempty"` + ConfigurationTenant string `json:"ConfigurationTenant,omitempty"` + RequiresApproval bool `json:"RequiresApproval,omitempty"` + FriendlyName string `json:"FriendlyName,omitempty"` } // EnrollmentPatternAssociatedRoleResponse represents associated role information in responses type EnrollmentPatternAssociatedRoleResponse struct { - Id *int `json:"Id,omitempty"` - Name *string `json:"Name,omitempty"` + Id int `json:"Id,omitempty"` + Name string `json:"Name,omitempty"` } // EnrollmentPatternCAResponse represents certificate authority information in responses type EnrollmentPatternCAResponse struct { - Id *int `json:"Id,omitempty"` - LogicalName *string `json:"LogicalName,omitempty"` - HostName *string `json:"HostName,omitempty"` - CAName *string `json:"CAName,omitempty"` + Id int `json:"Id,omitempty"` + LogicalName string `json:"LogicalName,omitempty"` + HostName string `json:"HostName,omitempty"` + ConfigurationTenant string `json:"ConfigurationTenant,omitempty"` } // AlgorithmDataRequestV2 represents algorithm configuration for requests @@ -207,9 +204,9 @@ type AlgorithmDataRequestV2 struct { // AlgorithmDataResponse represents algorithm configuration in responses type AlgorithmDataResponse struct { - KeyType *string `json:"KeyType,omitempty"` - KeySize *int `json:"KeySize,omitempty"` - CurveName *string `json:"CurveName,omitempty"` + Name string `json:"Name,omitempty"` + BitLengths []int `json:"bit_lengths,omitempty"` + Curves []string `json:"curves,omitempty"` } // EnrollmentPatternsQueryParams represents query parameters for listing enrollment patterns From 0a01a433b644e7c67cb673cb286a89a6d1ff9075 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 3 Dec 2025 08:59:22 -0800 Subject: [PATCH 17/51] chore(deps): Update all deps to latest releases. --- go.mod | 6 ++--- go.sum | 8 +++---- v2/go.mod | 42 +++++++++++++++++------------------ v2/go.sum | 50 +++++++++++++++++++++++++++++++++++++++++ v3/go.mod | 28 +++++++++++------------ v3/go.sum | 66 +++++++++++++++++++++++++------------------------------ 6 files changed, 121 insertions(+), 79 deletions(-) diff --git a/go.mod b/go.mod index 5f08992..e23dbd4 100644 --- a/go.mod +++ b/go.mod @@ -1,11 +1,11 @@ module github.com/Keyfactor/keyfactor-go-client -go 1.20 +go 1.24.0 require ( github.com/Keyfactor/keyfactor-go-client-sdk v1.0.2 github.com/spbsoluble/go-pkcs12 v0.3.3 - go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 + go.mozilla.org/pkcs7 v0.9.0 ) -require golang.org/x/crypto v0.11.0 // indirect +require golang.org/x/crypto v0.45.0 // indirect diff --git a/go.sum b/go.sum index 08ced4d..5d16674 100644 --- a/go.sum +++ b/go.sum @@ -2,7 +2,7 @@ github.com/Keyfactor/keyfactor-go-client-sdk v1.0.2 h1:caLlzFCz2L4Dth/9wh+VlypFA github.com/Keyfactor/keyfactor-go-client-sdk v1.0.2/go.mod h1:Z5pSk8YFGXHbKeQ1wTzVN8A4P/fZmtAwqu3NgBHbDOs= github.com/spbsoluble/go-pkcs12 v0.3.3 h1:3nh7IKn16RDpmrSMtOu1JvbB0XHYq1j+IsICdU1c7J4= github.com/spbsoluble/go-pkcs12 v0.3.3/go.mod h1:MAxKIUEIl/QVcua/I1L4Otyxl9UvLCCIktce2Tjz6Nw= -go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 h1:CCriYyAfq1Br1aIYettdHZTy8mBTIPo7We18TuO/bak= -go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= -golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= -golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +go.mozilla.org/pkcs7 v0.9.0 h1:yM4/HS9dYv7ri2biPtxt8ikvB37a980dg69/pKmS+eI= +go.mozilla.org/pkcs7 v0.9.0/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= diff --git a/v2/go.mod b/v2/go.mod index 3593a14..89eaeea 100644 --- a/v2/go.mod +++ b/v2/go.mod @@ -14,38 +14,36 @@ module github.com/Keyfactor/keyfactor-go-client/v2 -go 1.22 - -toolchain go1.23.2 +go 1.24.0 require ( - github.com/Keyfactor/keyfactor-auth-client-go v1.1.1-rc.0 + github.com/Keyfactor/keyfactor-auth-client-go v1.3.0 github.com/Keyfactor/keyfactor-go-client v1.4.3 - github.com/Keyfactor/keyfactor-go-client-sdk v1.0.1 - github.com/hashicorp/terraform-plugin-log v0.9.0 + github.com/Keyfactor/keyfactor-go-client-sdk v1.0.2 + github.com/hashicorp/terraform-plugin-log v0.10.0 github.com/spbsoluble/go-pkcs12 v0.3.3 go.mozilla.org/pkcs7 v0.9.0 ) require ( - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0 // indirect - github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 // indirect - github.com/fatih/color v1.13.0 // indirect - github.com/golang-jwt/jwt/v5 v5.2.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/google/uuid v1.6.0 // indirect - github.com/hashicorp/go-hclog v1.5.0 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/kylelemons/godebug v1.1.0 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect - golang.org/x/crypto v0.30.0 // indirect - golang.org/x/net v0.32.0 // indirect - golang.org/x/oauth2 v0.24.0 // indirect - golang.org/x/sys v0.28.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/crypto v0.45.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/oauth2 v0.33.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/v2/go.sum b/v2/go.sum index cd297b3..a1e7be6 100644 --- a/v2/go.sum +++ b/v2/go.sum @@ -1,25 +1,47 @@ github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0 h1:JZg6HRh6W6U4OLl6lk7BZ7BLisIzM9dG1R50zUk9C/M= github.com/Azure/azure-sdk-for-go/sdk/azcore v1.16.0/go.mod h1:YL1xnZ6QejvQHWJrX/AvhFl4WW4rqHVoKspWNVwFk0M= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0 h1:B/dfvscEQtew9dVuoxqxrUKKv8Ih2f55PydknDamU+g= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.0/go.mod h1:fiPSssYvltE08HJchL04dOy+RD4hgrjph0cwGGMntdI= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0 h1:+m0M/LFxN43KvULkDNfdXOgrjtg6UYJPFBJyuEcRCAw= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.0/go.mod h1:PwOyop78lveYMRs6oCxjiVyBdyCgIYH6XHIVZO9/SFQ= github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0 h1:ywEEhmNahHBihViHepv3xPBn1663uRv2t2q/ESv9seY= github.com/Azure/azure-sdk-for-go/sdk/internal v1.10.0/go.mod h1:iZDifYGJTIgIIkYRNWPENUnqx6bJ2xnSDFI2tjwZNuY= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.0 h1:WLUIpeyv04H0RCcQHaA4TNoyrQ39Ox7V+re+iaqzTe0= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.0/go.mod h1:hd8hTTIY3VmUVPRHNH7GVCHO3SHgXkJKZHReby/bnUQ= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.1/go.mod h1:hPv41DbqMmnxcGralanA/kVlfdH5jv3T4LxGku2E1BY= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0 h1:/g8S6wk65vfC6m3FIxJ+i5QDyN9JWwXI8Hb0Img10hU= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0/go.mod h1:gpl+q95AzZlKVI3xSoseF9QPrypk0hQqBiJYeB/cR/I= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0 h1:eXnN9kaS8TiDwXjoie3hMRLuwdUBUMW9KRgOqB3mCaw= github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.0/go.mod h1:XIpam8wumeZ5rVMuhdDQLMfIPDf1WO3IzrCRO3e3e3o= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2 h1:kYRSnvJju5gYVyhkij+RTJ/VR6QIUaCfWeaFm2ycsjQ= github.com/AzureAD/microsoft-authentication-library-for-go v1.3.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/Keyfactor/keyfactor-auth-client-go v1.1.1-rc.0 h1:/N/7pBj/oTUM1cYga2NvKyA4q6nfE0acciJHZqKC9Ug= github.com/Keyfactor/keyfactor-auth-client-go v1.1.1-rc.0/go.mod h1:yw92P9gSYVEyWkiUAJFsb7hjhXa8slN1+yTQgjSgovM= +github.com/Keyfactor/keyfactor-auth-client-go v1.3.0 h1:otC213b6CYzqeN9b3CRlH1Qj1hTFIN5nqPA8gTlHdLg= +github.com/Keyfactor/keyfactor-auth-client-go v1.3.0/go.mod h1:97vCisBNkdCK0l2TuvOSdjlpvQa4+GHsMut1UTyv1jo= github.com/Keyfactor/keyfactor-go-client v1.4.3 h1:CmGvWcuIbDRFM0PfYOQH6UdtAgplvZBpU++KTU8iseg= github.com/Keyfactor/keyfactor-go-client v1.4.3/go.mod h1:3ZymLNCaSazglcuYeNfm9nrzn22wcwLjIWURrnUygBo= github.com/Keyfactor/keyfactor-go-client-sdk v1.0.1 h1:cs8hhvsY3MJ2o1K11HLTRCjRT8SbsKhhi73Y4By2CI0= github.com/Keyfactor/keyfactor-go-client-sdk v1.0.1/go.mod h1:Z5pSk8YFGXHbKeQ1wTzVN8A4P/fZmtAwqu3NgBHbDOs= +github.com/Keyfactor/keyfactor-go-client-sdk v1.0.2 h1:caLlzFCz2L4Dth/9wh+VlypFATmOMmCSQkCPKOKMxw8= +github.com/Keyfactor/keyfactor-go-client-sdk v1.0.2/go.mod h1:Z5pSk8YFGXHbKeQ1wTzVN8A4P/fZmtAwqu3NgBHbDOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -29,16 +51,25 @@ github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/r github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= github.com/golang-jwt/jwt/v5 v5.2.1 h1:OuVbFODueb089Lh128TAcimifWaLhJwVflnrgM17wHk= github.com/golang-jwt/jwt/v5 v5.2.1/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= +github.com/hashicorp/terraform-plugin-log v0.10.0 h1:eu2kW6/QBVdN4P3Ju2WiB2W3ObjkAsyfBsL3Wh1fj3g= +github.com/hashicorp/terraform-plugin-log v0.10.0/go.mod h1:/9RR5Cv2aAbrqcTSdNmY1NRHP4E3ekrXRGjqORpXyB0= github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6 h1:IsMZxCuZqKuao2vNdfD82fjjgPLfyHLpR41Z88viRWs= github.com/keybase/go-keychain v0.0.0-20231219164618-57a3676c3af6/go.mod h1:3VeWNIJaW+O5xpRQbPp0Ybqu1vJd/pm7s2F473HRrkw= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -51,11 +82,15 @@ github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ= @@ -76,10 +111,19 @@ go.mozilla.org/pkcs7 v0.9.0 h1:yM4/HS9dYv7ri2biPtxt8ikvB37a980dg69/pKmS+eI= go.mozilla.org/pkcs7 v0.9.0/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY= golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= golang.org/x/net v0.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= +golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -90,8 +134,14 @@ golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= diff --git a/v3/go.mod b/v3/go.mod index d69aa62..70dc405 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -14,36 +14,36 @@ module github.com/Keyfactor/keyfactor-go-client/v3 -go 1.24 +go 1.24.0 toolchain go1.24.5 require ( github.com/Keyfactor/keyfactor-auth-client-go v1.3.0 - github.com/hashicorp/terraform-plugin-log v0.9.0 + github.com/hashicorp/terraform-plugin-log v0.10.0 github.com/spbsoluble/go-pkcs12 v0.3.3 go.mozilla.org/pkcs7 v0.9.0 ) require ( - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 // indirect - github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 // indirect - github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 // indirect - github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.1 // indirect - github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 // indirect - github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 // indirect github.com/fatih/color v1.18.0 // indirect - github.com/golang-jwt/jwt/v5 v5.2.2 // indirect + github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect - golang.org/x/crypto v0.39.0 // indirect - golang.org/x/net v0.41.0 // indirect - golang.org/x/oauth2 v0.30.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.26.0 // indirect + golang.org/x/crypto v0.45.0 // indirect + golang.org/x/net v0.47.0 // indirect + golang.org/x/oauth2 v0.33.0 // indirect + golang.org/x/sys v0.38.0 // indirect + golang.org/x/text v0.31.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/v3/go.sum b/v3/go.sum index 2008907..ede870e 100644 --- a/v3/go.sum +++ b/v3/go.sum @@ -1,41 +1,37 @@ -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0 h1:Gt0j3wceWMwPmiazCa8MzMA0MfhmPIz0Qp0FJ6qcM0U= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.18.0/go.mod h1:Ot/6aikWnKWi4l9QB7qVSwa8iMphQNqkWALMoNT3rzM= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1 h1:B+blDbyVIG3WaikNxPnhPiJ1MThR03b3vKGtER95TP4= -github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.10.1/go.mod h1:JdM5psgjfBf5fo2uWOZhflPWyDBZ/O/CNAH9CtsuZE4= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2/go.mod h1:Pa9ZNPuoNu/GztvBSKk9J1cDJW6vk/n0zLtV4mgd8N8= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1 h1:FPKJS1T+clwv+OLGt13a8UjqeRuh0O4SJ3lUriThc+4= -github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.1/go.mod h1:j2chePtV91HrC22tGoRX3sGY42uF13WzmmV80/OdVAA= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.1 h1:mrkDCdkMsD4l9wjFGhofFHFrV43Y3c53RSLKOCJ5+Ow= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.3.1/go.mod h1:hPv41DbqMmnxcGralanA/kVlfdH5jv3T4LxGku2E1BY= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1 h1:bFWuoEKg+gImo7pvkiQEFAc8ocibADgXeiLAxWhWmkI= -github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.1.1/go.mod h1:Vih/3yc6yac2JzU4hzpaDupBJP0Flaia9rXXrU8xyww= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 h1:9iefClla7iYpfYWdzPCRDozdmndjTm8DXdpCzPajMgA= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2/go.mod h1:XtLgD3ZD34DAaVIIAyG3objl5DynM3CQ/vMcbBNJZGI= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0 h1:/g8S6wk65vfC6m3FIxJ+i5QDyN9JWwXI8Hb0Img10hU= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0/go.mod h1:gpl+q95AzZlKVI3xSoseF9QPrypk0hQqBiJYeB/cR/I= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0 h1:nCYfgcSyHZXJI8J0IWE5MsCGlb2xp9fJiXyxWgmOFg4= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.2.0/go.mod h1:ucUjca2JtSZboY8IoUqyQyuuXvwbMBVwFOm0vdQPNhA= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJTmL004Abzc5wDB5VtZG2PJk5ndYDgVacGqfirKxjM= github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= -github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2 h1:oygO0locgZJe7PpYPXT5A29ZkwJaPqcva7BVeemZOZs= -github.com/AzureAD/microsoft-authentication-library-for-go v1.4.2/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= +github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= github.com/Keyfactor/keyfactor-auth-client-go v1.3.0 h1:otC213b6CYzqeN9b3CRlH1Qj1hTFIN5nqPA8gTlHdLg= github.com/Keyfactor/keyfactor-auth-client-go v1.3.0/go.mod h1:97vCisBNkdCK0l2TuvOSdjlpvQa4+GHsMut1UTyv1jo= -github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= -github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= -github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= -github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8= -github.com/golang-jwt/jwt/v5 v5.2.2/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo= +github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= -github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= -github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= +github.com/hashicorp/terraform-plugin-log v0.10.0 h1:eu2kW6/QBVdN4P3Ju2WiB2W3ObjkAsyfBsL3Wh1fj3g= +github.com/hashicorp/terraform-plugin-log v0.10.0/go.mod h1:/9RR5Cv2aAbrqcTSdNmY1NRHP4E3ekrXRGjqORpXyB0= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -58,24 +54,22 @@ github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmd github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/redis/go-redis/v9 v9.8.0 h1:q3nRvjrlge/6UD7eTu/DSg2uYiU2mCL0G/uzBWqhicI= -github.com/redis/go-redis/v9 v9.8.0/go.mod h1:huWgSWd8mW6+m0VPhJjSSQ+d6Nh1VICQ6Q5lHuCH/Iw= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/spbsoluble/go-pkcs12 v0.3.3 h1:3nh7IKn16RDpmrSMtOu1JvbB0XHYq1j+IsICdU1c7J4= github.com/spbsoluble/go-pkcs12 v0.3.3/go.mod h1:MAxKIUEIl/QVcua/I1L4Otyxl9UvLCCIktce2Tjz6Nw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= go.mozilla.org/pkcs7 v0.9.0 h1:yM4/HS9dYv7ri2biPtxt8ikvB37a980dg69/pKmS+eI= go.mozilla.org/pkcs7 v0.9.0/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= -golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= -golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= -golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= -golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= +golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= +golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= +golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= +golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= +golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= +golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -83,10 +77,10 @@ golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= +golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= +golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= From e2548ef8da16176b554cd714a73d740d488dcf93 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 3 Dec 2025 09:13:07 -0800 Subject: [PATCH 18/51] fix(models): UpdateStoreFctArgs to use UpdateStorePasswordConfig for Password --- v3/api/store_models.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/v3/api/store_models.go b/v3/api/store_models.go index a210cad..bf4db2c 100644 --- a/v3/api/store_models.go +++ b/v3/api/store_models.go @@ -51,14 +51,14 @@ type UpdateStoreFctArgs struct { // automatically populated by the CreateStore method. However, if configured, this field will be used. PropertiesString string `json:"Properties,omitempty"` // Mapped name-value pair field used to configure properties. - Properties map[string]interface{} `json:"-"` - AgentId string `json:"AgentId"` - AgentAssigned *bool `json:"AgentAssigned,omitempty"` - ContainerName *string `json:"ContainerName,omitempty"` - InventorySchedule *InventorySchedule `json:"InventorySchedule,omitempty"` - ReEnrollmentStatus *ReEnrollmnentConfig `json:"ReEnrollmentStatus,omitempty"` - SetNewPasswordAllowed *bool `json:"SetNewPasswordAllowed,omitempty"` - Password *StorePasswordConfig `json:"Password"` + Properties map[string]interface{} `json:"-"` + AgentId string `json:"AgentId"` + AgentAssigned *bool `json:"AgentAssigned,omitempty"` + ContainerName *string `json:"ContainerName,omitempty"` + InventorySchedule *InventorySchedule `json:"InventorySchedule,omitempty"` + ReEnrollmentStatus *ReEnrollmnentConfig `json:"ReEnrollmentStatus,omitempty"` + SetNewPasswordAllowed *bool `json:"SetNewPasswordAllowed,omitempty"` + Password *UpdateStorePasswordConfig `json:"Password"` } type UpdateStorePasswordConfig struct { From f6ac9087776f1a03d2db01f6de77fdac087a2a64 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 4 Dec 2025 09:02:00 -0800 Subject: [PATCH 19/51] feat(models): Add models for Pam/Types API --- v3/api/pam_types_models.go | 103 +++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) create mode 100644 v3/api/pam_types_models.go diff --git a/v3/api/pam_types_models.go b/v3/api/pam_types_models.go new file mode 100644 index 0000000..d165df0 --- /dev/null +++ b/v3/api/pam_types_models.go @@ -0,0 +1,103 @@ +// Copyright 2025 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +// PamParameterDataType represents the data type of a PAM parameter +// 1 = string, 2 = secret +type PamParameterDataType int + +const ( + PamParameterDataTypeString PamParameterDataType = 1 + PamParameterDataTypeSecret PamParameterDataType = 2 +) + +// SecretType represents the type of secret in the system +// 0-4 are valid values +type SecretType int + +// ProviderTypeParameterResponse represents a parameter for a PAM provider type +type ProviderTypeParameterResponse struct { + Id int `json:"Id,omitempty"` + Name *string `json:"Name,omitempty"` + DisplayName *string `json:"DisplayName,omitempty"` + DataType PamParameterDataType `json:"DataType,omitempty"` + InstanceLevel bool `json:"InstanceLevel,omitempty"` +} + +// ProviderTypeResponse represents a PAM provider type +type ProviderTypeResponse struct { + Id string `json:"Id,omitempty"` // UUID format + Name *string `json:"Name,omitempty"` + Parameters *[]ProviderTypeParameterResponse `json:"Parameters,omitempty"` +} + +// ProviderTypeParameterCreateRequest represents a request to create a PAM provider type parameter +type ProviderTypeParameterCreateRequest struct { + Name string `json:"Name"` + DisplayName *string `json:"DisplayName,omitempty"` + DataType PamParameterDataType `json:"DataType,omitempty"` + InstanceLevel bool `json:"InstanceLevel,omitempty"` +} + +// ProviderTypeCreateRequest represents a request to create a PAM provider type +type ProviderTypeCreateRequest struct { + Name string `json:"Name"` + Parameters *[]ProviderTypeParameterCreateRequest `json:"Parameters,omitempty"` +} + +// ProviderCreateRequestProviderTypeParam represents a provider type parameter in a provider creation request +type ProviderCreateRequestProviderTypeParam struct { + Id int `json:"Id,omitempty"` + Name *string `json:"Name,omitempty"` + DisplayName *string `json:"DisplayName,omitempty"` + InstanceLevel bool `json:"InstanceLevel,omitempty"` +} + +// PamProviderTypeParam represents a provider type parameter (full model) for PAM operations +type PamProviderTypeParam struct { + Id int `json:"Id,omitempty"` + Name *string `json:"Name,omitempty"` + DisplayName *string `json:"DisplayName,omitempty"` + DataType PamParameterDataType `json:"DataType,omitempty"` + InstanceLevel bool `json:"InstanceLevel,omitempty"` + ProviderType *ProviderType `json:"ProviderType,omitempty"` +} + +// ProviderType represents a PAM provider type (full model) +type ProviderType struct { + Id string `json:"Id,omitempty"` // UUID format + Name *string `json:"Name,omitempty"` + ProviderTypeParams *[]PamProviderTypeParam `json:"ProviderTypeParams,omitempty"` +} + +// PamProviderTypeParamValue represents a parameter value for a PAM provider type +type PamProviderTypeParamValue struct { + Id int `json:"Id,omitempty"` + Value *string `json:"Value,omitempty"` + ParameterId int `json:"ParameterId,omitempty"` + InstanceId *int `json:"InstanceId,omitempty"` + InstanceGuid *string `json:"InstanceGuid,omitempty"` // UUID format + Provider *Provider `json:"Provider,omitempty"` + ProviderTypeParam *PamProviderTypeParam `json:"ProviderTypeParam,omitempty"` +} + +// PamProviderTypeParamValueResponse represents a parameter value response for a PAM provider type +type PamProviderTypeParamValueResponse struct { + Id int `json:"Id,omitempty"` + Value *string `json:"Value,omitempty"` + InstanceId *int `json:"InstanceId,omitempty"` + InstanceGuid *string `json:"InstanceGuid,omitempty"` // UUID format + ProviderTypeParam *ProviderTypeParameterResponse `json:"ProviderTypeParam,omitempty"` +} From 362fe173d7dfddac6b6b3062566179e7ee582900 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 4 Dec 2025 09:02:39 -0800 Subject: [PATCH 20/51] feat(api): Add Pam/Types CRUD API functions. --- v3/api/pam_types.go | 159 ++++++++++++++ v3/api/pam_types_test.go | 432 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 591 insertions(+) create mode 100644 v3/api/pam_types.go create mode 100644 v3/api/pam_types_test.go diff --git a/v3/api/pam_types.go b/v3/api/pam_types.go new file mode 100644 index 0000000..3aa78f7 --- /dev/null +++ b/v3/api/pam_types.go @@ -0,0 +1,159 @@ +// Copyright 2025 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "encoding/json" + "fmt" + "log" +) + +// ListPAMProviderTypes returns all PAM provider types in the Keyfactor instance +func (c *Client) ListPAMProviderTypes() (*[]ProviderTypeResponse, error) { + log.Println("[INFO] Listing all PAM provider types") + + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + }, + } + + keyfactorAPIStruct := &request{ + Method: "GET", + Endpoint: "PamProviders/Types", + Headers: headers, + Payload: nil, + } + + resp, err := c.sendRequest(keyfactorAPIStruct) + if err != nil { + return nil, err + } + + var jsonResp []ProviderTypeResponse + err = json.NewDecoder(resp.Body).Decode(&jsonResp) + if err != nil { + return nil, err + } + return &jsonResp, nil +} + +func (c *Client) GetPAMProviderTypeByName(name string) (*ProviderTypeResponse, error) { + // list all provider types + types, err := c.ListPAMProviderTypes() + if err != nil { + return nil, err + } + + // find the provider type with the matching name + for _, t := range *types { + if t.Name != nil && *t.Name == name { + return &t, nil + } + } + return nil, fmt.Errorf("PAM provider type with name '%s' not found", name) +} + +// GetPAMProviderType returns a specific PAM provider type by ID +func (c *Client) GetPAMProviderType(id string) (*ProviderTypeResponse, error) { + log.Printf("[INFO] Getting PAM provider type with ID: %s", id) + + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + }, + } + + endpoint := fmt.Sprintf("PamProviders/Types/%s", id) + keyfactorAPIStruct := &request{ + Method: "GET", + Endpoint: endpoint, + Headers: headers, + Payload: nil, + } + + resp, err := c.sendRequest(keyfactorAPIStruct) + if err != nil { + return nil, err + } + + var jsonResp ProviderTypeResponse + err = json.NewDecoder(resp.Body).Decode(&jsonResp) + if err != nil { + return nil, err + } + return &jsonResp, nil +} + +// CreatePAMProviderType creates a new PAM provider type with the associated properties +func (c *Client) CreatePAMProviderType(providerType *ProviderTypeCreateRequest) (*ProviderTypeResponse, error) { + log.Printf("[INFO] Creating new PAM provider type: %s", providerType.Name) + + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + {"Content-Type", "application/json"}, + }, + } + + keyfactorAPIStruct := &request{ + Method: "POST", + Endpoint: "PamProviders/Types", + Headers: headers, + Payload: providerType, + } + + resp, err := c.sendRequest(keyfactorAPIStruct) + if err != nil { + return nil, err + } + + var jsonResp ProviderTypeResponse + err = json.NewDecoder(resp.Body).Decode(&jsonResp) + if err != nil { + return nil, err + } + return &jsonResp, nil +} + +// DeletePAMProviderType deletes a PAM provider type by ID, as long as it's not currently in use +func (c *Client) DeletePAMProviderType(id string) error { + log.Printf("[INFO] Deleting PAM provider type with ID: %s", id) + + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + }, + } + + endpoint := fmt.Sprintf("PamProviders/Types/%s", id) + keyfactorAPIStruct := &request{ + Method: "DELETE", + Endpoint: endpoint, + Headers: headers, + Payload: nil, + } + + _, err := c.sendRequest(keyfactorAPIStruct) + if err != nil { + return err + } + + return nil +} diff --git a/v3/api/pam_types_test.go b/v3/api/pam_types_test.go new file mode 100644 index 0000000..eac814a --- /dev/null +++ b/v3/api/pam_types_test.go @@ -0,0 +1,432 @@ +// Copyright 2025 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers" +) + +// mockAuthConfig implements AuthConfig interface for testing +type mockAuthConfig struct { + serverConfig *auth_providers.Server + httpClient *http.Client +} + +func (m *mockAuthConfig) GetServerConfig() *auth_providers.Server { + return m.serverConfig +} + +func (m *mockAuthConfig) GetHttpClient() (*http.Client, error) { + return m.httpClient, nil +} + +func (m *mockAuthConfig) Authenticate() error { + return nil +} + +// newTestClient creates a test client with mock server +func newTestClient(server *httptest.Server) *Client { + return &Client{ + AuthClient: &mockAuthConfig{ + serverConfig: &auth_providers.Server{ + Host: server.URL, + APIPath: "/KeyfactorAPI", + SkipTLSVerify: true, + }, + httpClient: server.Client(), + }, + } +} + +// Mock response data +var ( + mockProviderTypeId = "550e8400-e29b-41d4-a716-446655440000" + mockProviderTypeName = "CyberArk" + + mockProviderTypeResponse = ProviderTypeResponse{ + Id: mockProviderTypeId, + Name: &mockProviderTypeName, + Parameters: &[]ProviderTypeParameterResponse{ + { + Id: 1, + Name: stringPtr("Username"), + DisplayName: stringPtr("User Name"), + DataType: PamParameterDataTypeString, + InstanceLevel: false, + }, + { + Id: 2, + Name: stringPtr("Password"), + DisplayName: stringPtr("Password"), + DataType: PamParameterDataTypeSecret, + InstanceLevel: true, + }, + }, + } + + mockProviderResponseLegacy = ProviderResponseLegacy{ + Id: 1, + Name: stringPtr("Test Provider"), + Area: 1, + ProviderType: &ProviderType{ + Id: mockProviderTypeId, + Name: &mockProviderTypeName, + }, + SecuredAreaId: intPtr(1), + Remote: false, + } + + mockLocalPAMEntry = LocalPAMEntryResponse{ + ProviderId: 1, + SecretName: stringPtr("test-secret"), + Description: stringPtr("Test secret description"), + } +) + +// Helper functions +func stringPtr(s string) *string { + return &s +} + +func intPtr(i int) *int { + return &i +} + +func TestListPAMProviderTypes(t *testing.T) { + tests := []struct { + name string + mockResponse interface{} + mockStatusCode int + wantErr bool + wantCount int + }{ + { + name: "successful list", + mockResponse: []ProviderTypeResponse{ + mockProviderTypeResponse, + }, + mockStatusCode: http.StatusOK, + wantErr: false, + wantCount: 1, + }, + { + name: "empty list", + mockResponse: []ProviderTypeResponse{}, + mockStatusCode: http.StatusOK, + wantErr: false, + wantCount: 0, + }, + { + name: "server error", + mockResponse: map[string]string{"error": "internal server error"}, + mockStatusCode: http.StatusInternalServerError, + wantErr: true, + wantCount: 0, + }, + } + + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + server := httptest.NewTLSServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + // Verify request + if r.URL.Path != "/KeyfactorAPI/PamProviders/Types" { + t.Errorf("Expected path /KeyfactorAPI/PamProviders/Types, got %s", r.URL.Path) + } + if r.Method != "GET" { + t.Errorf("Expected GET method, got %s", r.Method) + } + + w.WriteHeader(tt.mockStatusCode) + _ = json.NewEncoder(w).Encode(tt.mockResponse) + }, + ), + ) + defer server.Close() + + client := newTestClient(server) + + got, err := client.ListPAMProviderTypes() + if (err != nil) != tt.wantErr { + t.Errorf("ListPAMProviderTypes() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && got != nil { + if len(*got) != tt.wantCount { + t.Errorf("ListPAMProviderTypes() got %d items, want %d", len(*got), tt.wantCount) + } + } + }, + ) + } +} + +func TestGetPAMProviderType(t *testing.T) { + tests := []struct { + name string + providerId string + mockResponse interface{} + mockStatusCode int + wantErr bool + }{ + { + name: "successful get", + providerId: mockProviderTypeId, + mockResponse: mockProviderTypeResponse, + mockStatusCode: http.StatusOK, + wantErr: false, + }, + { + name: "not found", + providerId: "nonexistent-id", + mockResponse: map[string]string{"error": "not found"}, + mockStatusCode: http.StatusNotFound, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + server := httptest.NewTLSServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + expectedPath := "/KeyfactorAPI/PamProviders/Types/" + tt.providerId + if r.URL.Path != expectedPath { + t.Errorf("Expected path %s, got %s", expectedPath, r.URL.Path) + } + if r.Method != "GET" { + t.Errorf("Expected GET method, got %s", r.Method) + } + + w.WriteHeader(tt.mockStatusCode) + _ = json.NewEncoder(w).Encode(tt.mockResponse) + }, + ), + ) + defer server.Close() + + client := newTestClient(server) + + got, err := client.GetPAMProviderType(tt.providerId) + if (err != nil) != tt.wantErr { + t.Errorf("GetPAMProviderType() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && got != nil { + if got.Id != mockProviderTypeId { + t.Errorf("GetPAMProviderType() Id = %v, want %v", got.Id, mockProviderTypeId) + } + } + }, + ) + } +} + +func TestCreatePAMProviderType(t *testing.T) { + createRequest := &ProviderTypeCreateRequest{ + Name: "New Provider Type", + Parameters: &[]ProviderTypeParameterCreateRequest{ + { + Name: "ApiKey", + DisplayName: stringPtr("API Key"), + DataType: PamParameterDataTypeSecret, + InstanceLevel: true, + }, + }, + } + + tests := []struct { + name string + request *ProviderTypeCreateRequest + mockResponse interface{} + mockStatusCode int + wantErr bool + }{ + { + name: "successful create", + request: createRequest, + mockResponse: mockProviderTypeResponse, + mockStatusCode: http.StatusOK, + wantErr: false, + }, + { + name: "bad request", + request: createRequest, + mockResponse: map[string]string{"error": "invalid request"}, + mockStatusCode: http.StatusBadRequest, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + server := httptest.NewTLSServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/KeyfactorAPI/PamProviders/Types" { + t.Errorf("Expected path /KeyfactorAPI/PamProviders/Types, got %s", r.URL.Path) + } + if r.Method != "POST" { + t.Errorf("Expected POST method, got %s", r.Method) + } + + // Verify request body + var receivedRequest ProviderTypeCreateRequest + if err := json.NewDecoder(r.Body).Decode(&receivedRequest); err != nil { + t.Errorf("Failed to decode request body: %v", err) + } + if receivedRequest.Name != tt.request.Name { + t.Errorf("Expected name %s, got %s", tt.request.Name, receivedRequest.Name) + } + + w.WriteHeader(tt.mockStatusCode) + _ = json.NewEncoder(w).Encode(tt.mockResponse) + }, + ), + ) + defer server.Close() + + client := newTestClient(server) + + got, err := client.CreatePAMProviderType(tt.request) + if (err != nil) != tt.wantErr { + t.Errorf("CreatePAMProviderType() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && got != nil { + if got.Id != mockProviderTypeId { + t.Errorf("CreatePAMProviderType() Id = %v, want %v", got.Id, mockProviderTypeId) + } + } + }, + ) + } +} + +func TestDeletePAMProviderType(t *testing.T) { + tests := []struct { + name string + providerId string + mockStatusCode int + wantErr bool + }{ + { + name: "successful delete", + providerId: mockProviderTypeId, + mockStatusCode: http.StatusNoContent, + wantErr: false, + }, + { + name: "not found", + providerId: "nonexistent-id", + mockStatusCode: http.StatusNotFound, + wantErr: true, + }, + { + name: "in use", + providerId: mockProviderTypeId, + mockStatusCode: http.StatusConflict, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + server := httptest.NewTLSServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + expectedPath := "/KeyfactorAPI/PamProviders/Types/" + tt.providerId + if r.URL.Path != expectedPath { + t.Errorf("Expected path %s, got %s", expectedPath, r.URL.Path) + } + if r.Method != "DELETE" { + t.Errorf("Expected DELETE method, got %s", r.Method) + } + + w.WriteHeader(tt.mockStatusCode) + }, + ), + ) + defer server.Close() + + client := newTestClient(server) + + err := client.DeletePAMProviderType(tt.providerId) + if (err != nil) != tt.wantErr { + t.Errorf("DeletePAMProviderType() error = %v, wantErr %v", err, tt.wantErr) + } + }, + ) + } +} + +func TestGetPAMProviderQuery_toQueryString(t *testing.T) { + tests := []struct { + name string + query *GetPAMProviderQuery + want string + }{ + { + name: "nil query", + query: nil, + want: "", + }, + { + name: "empty query", + query: &GetPAMProviderQuery{}, + want: "", + }, + { + name: "full query", + query: &GetPAMProviderQuery{ + QueryString: "Name -eq 'Test'", + PageReturned: 1, + ReturnLimit: 10, + SortField: "Name", + SortAscending: 0, + }, + want: "QueryString=Name -eq 'Test'&PageReturned=1&ReturnLimit=10&SortField=Name&SortAscending=0", + }, + { + name: "partial query", + query: &GetPAMProviderQuery{ + QueryString: "Name -eq 'Test'", + ReturnLimit: 10, + }, + want: "QueryString=Name -eq 'Test'&ReturnLimit=10", + }, + } + + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + got := tt.query.toQueryString() + if got != tt.want { + t.Errorf("GetPAMProviderQuery.toQueryString() = %v, want %v", got, tt.want) + } + }, + ) + } +} From f8bf9630c8892c880bed9f78590c0bc96aef2be6 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 4 Dec 2025 09:03:02 -0800 Subject: [PATCH 21/51] feat(models): Add Pam provider API models --- v3/api/pam_models.go | 102 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 v3/api/pam_models.go diff --git a/v3/api/pam_models.go b/v3/api/pam_models.go new file mode 100644 index 0000000..7c675e4 --- /dev/null +++ b/v3/api/pam_models.go @@ -0,0 +1,102 @@ +package api + +// Provider represents a PAM provider (full model) +type Provider struct { + Id int `json:"Id,omitempty"` + Name string `json:"Name"` + Area int `json:"Area,omitempty"` + ProviderType ProviderType `json:"ProviderType"` + ProviderTypeParamValues *[]PamProviderTypeParamValue `json:"ProviderTypeParamValues,omitempty"` + SecuredAreaId *int `json:"SecuredAreaId,omitempty"` + Remote bool `json:"Remote,omitempty"` + IsInUse bool `json:"IsInUse,omitempty"` + IsLocalDB bool `json:"IsLocalDB,omitempty"` +} + +// ProviderCreateRequestTypeParamValue represents a parameter value in a provider creation request +type ProviderCreateRequestTypeParamValue struct { + Id int `json:"Id,omitempty"` + Value *string `json:"Value,omitempty"` + InstanceId *int `json:"InstanceId,omitempty"` + InstanceGuid *string `json:"InstanceGuid,omitempty"` // UUID format + ProviderTypeParam *ProviderCreateRequestProviderTypeParam `json:"ProviderTypeParam,omitempty"` +} + +// ProviderCreateRequestProviderType represents a provider type reference in a provider creation request +type ProviderCreateRequestProviderType struct { + Id string `json:"Id,omitempty"` // UUID format +} + +// ProviderCreateRequest represents a request to create a PAM provider +type ProviderCreateRequest struct { + Name string `json:"Name"` + Remote bool `json:"Remote,omitempty"` + Area int `json:"Area,omitempty"` + ProviderType ProviderType `json:"ProviderType"` + ProviderTypeParamValues *[]PamProviderTypeParamValue `json:"ProviderTypeParamValues,omitempty"` + SecuredAreaId *int `json:"SecuredAreaId,omitempty"` +} + +// ProviderUpdateRequestLegacy represents a request to update a PAM provider (legacy format) +type ProviderUpdateRequestLegacy struct { + Id int `json:"Id"` + Name string `json:"Name"` + Remote bool `json:"Remote,omitempty"` + Area int `json:"Area,omitempty"` + ProviderType ProviderType `json:"ProviderType"` + ProviderTypeParamValues *[]PamProviderTypeParamValue `json:"ProviderTypeParamValues,omitempty"` + SecuredAreaId *int `json:"SecuredAreaId,omitempty"` +} + +// ProviderResponseLegacy represents a PAM provider response (legacy format) +type ProviderResponseLegacy struct { + Id int `json:"Id,omitempty"` + Name *string `json:"Name,omitempty"` + Area int `json:"Area,omitempty"` + ProviderType *ProviderType `json:"ProviderType,omitempty"` + ProviderTypeParamValues *[]PamProviderTypeParamValueResponse `json:"ProviderTypeParamValues,omitempty"` + SecuredAreaId *int `json:"SecuredAreaId,omitempty"` + Remote bool `json:"Remote,omitempty"` +} + +// LocalPAMEntryCreateRequest represents a request to create a local PAM entry +type LocalPAMEntryCreateRequest struct { + SecretName string `json:"SecretName"` + Description *string `json:"Description,omitempty"` + SecretValue string `json:"SecretValue"` +} + +// LocalPAMEntryUpdateRequest represents a request to update a local PAM entry +type LocalPAMEntryUpdateRequest struct { + SecretName string `json:"SecretName"` + Description *string `json:"Description,omitempty"` + SecretValue *string `json:"SecretValue,omitempty"` +} + +// LocalPAMEntryResponse represents a local PAM entry response +type LocalPAMEntryResponse struct { + ProviderId int `json:"ProviderId,omitempty"` + SecretName *string `json:"SecretName,omitempty"` + Description *string `json:"Description,omitempty"` +} + +// KeyfactorSecret represents a Keyfactor secret +type KeyfactorSecret struct { + Value interface{} `json:"Value,omitempty"` + SecretTypeGuid string `json:"SecretTypeGuid,omitempty"` // UUID format + InstanceId *int `json:"InstanceId,omitempty"` + InstanceGuid *string `json:"InstanceGuid,omitempty"` // UUID format + ProviderTypeParameterValues *[]PamProviderTypeParamValue `json:"ProviderTypeParameterValues,omitempty"` + ProviderId *int `json:"ProviderId,omitempty"` + IsManaged bool `json:"IsManaged,omitempty"` + SecretType SecretType `json:"SecretType,omitempty"` + RemoteProviderName *string `json:"RemoteProviderName,omitempty"` + HasValue bool `json:"HasValue,omitempty"` +} + +// KeyfactorAPISecret represents a Keyfactor API secret +type KeyfactorAPISecret struct { + SecretValue *string `json:"SecretValue,omitempty"` + Parameters map[string]*string `json:"Parameters,omitempty"` + Provider *int `json:"Provider,omitempty"` +} From 3580c64bef6bc18772f5a08e97401efaf4da05a8 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 4 Dec 2025 09:03:21 -0800 Subject: [PATCH 22/51] feat(api): Add Pam provider CRUD API functions --- v3/api/pam.go | 347 +++++++++++++++++++ v3/api/pam_test.go | 831 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1178 insertions(+) create mode 100644 v3/api/pam.go create mode 100644 v3/api/pam_test.go diff --git a/v3/api/pam.go b/v3/api/pam.go new file mode 100644 index 0000000..bb6fb49 --- /dev/null +++ b/v3/api/pam.go @@ -0,0 +1,347 @@ +package api + +import ( + "encoding/json" + "fmt" + "log" +) + +// ListPAMProviders returns all PAM providers according to the provided filter and output parameters +func (c *Client) ListPAMProviders(query *GetPAMProviderQuery) (*[]ProviderResponseLegacy, error) { + log.Println("[INFO] Listing all PAM providers") + + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + }, + } + + endpoint := "PamProviders" + if query != nil { + queryParams := query.toQueryString() + if queryParams != "" { + endpoint = fmt.Sprintf("%s?%s", endpoint, queryParams) + } + } + + keyfactorAPIStruct := &request{ + Method: "GET", + Endpoint: endpoint, + Headers: headers, + Payload: nil, + } + + resp, err := c.sendRequest(keyfactorAPIStruct) + if err != nil { + return nil, err + } + + var jsonResp []ProviderResponseLegacy + err = json.NewDecoder(resp.Body).Decode(&jsonResp) + if err != nil { + return nil, err + } + return &jsonResp, nil +} + +// GetPAMProvider returns a specific PAM provider by ID +func (c *Client) GetPAMProvider(id int) (*ProviderResponseLegacy, error) { + log.Printf("[INFO] Getting PAM provider with ID: %d", id) + + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + }, + } + + endpoint := fmt.Sprintf("PamProviders/%d", id) + keyfactorAPIStruct := &request{ + Method: "GET", + Endpoint: endpoint, + Headers: headers, + Payload: nil, + } + + resp, err := c.sendRequest(keyfactorAPIStruct) + if err != nil { + return nil, err + } + + var jsonResp ProviderResponseLegacy + err = json.NewDecoder(resp.Body).Decode(&jsonResp) + if err != nil { + return nil, err + } + return &jsonResp, nil +} + +// CreatePAMProvider creates a new PAM provider with the associated properties +func (c *Client) CreatePAMProvider(provider *ProviderCreateRequest) (*ProviderResponseLegacy, error) { + log.Printf("[INFO] Creating new PAM provider: %s", provider.Name) + + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + {"Content-Type", "application/json"}, + }, + } + + keyfactorAPIStruct := &request{ + Method: "POST", + Endpoint: "PamProviders", + Headers: headers, + Payload: provider, + } + + resp, err := c.sendRequest(keyfactorAPIStruct) + if err != nil { + return nil, err + } + + var jsonResp ProviderResponseLegacy + err = json.NewDecoder(resp.Body).Decode(&jsonResp) + if err != nil { + return nil, err + } + return &jsonResp, nil +} + +// UpdatePAMProvider updates an existing PAM provider +func (c *Client) UpdatePAMProvider(provider *ProviderUpdateRequestLegacy) (*ProviderResponseLegacy, error) { + log.Printf("[INFO] Updating PAM provider with ID: %d", provider.Id) + + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + {"Content-Type", "application/json"}, + }, + } + + keyfactorAPIStruct := &request{ + Method: "PUT", + Endpoint: "PamProviders", + Headers: headers, + Payload: provider, + } + + resp, err := c.sendRequest(keyfactorAPIStruct) + if err != nil { + return nil, err + } + + var jsonResp ProviderResponseLegacy + err = json.NewDecoder(resp.Body).Decode(&jsonResp) + if err != nil { + return nil, err + } + return &jsonResp, nil +} + +// DeletePAMProvider deletes a PAM provider by ID +func (c *Client) DeletePAMProvider(id int) error { + log.Printf("[INFO] Deleting PAM provider with ID: %d", id) + + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + }, + } + + endpoint := fmt.Sprintf("PamProviders/%d", id) + keyfactorAPIStruct := &request{ + Method: "DELETE", + Endpoint: endpoint, + Headers: headers, + Payload: nil, + } + + _, err := c.sendRequest(keyfactorAPIStruct) + if err != nil { + return err + } + + return nil +} + +// ListLocalPAMEntries returns local PAM entries for the given PAM provider according to the provided filter +func (c *Client) ListLocalPAMEntries(providerId int, query *GetPAMProviderQuery) (*[]LocalPAMEntryResponse, error) { + log.Printf("[INFO] Listing local PAM entries for provider ID: %d", providerId) + + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + }, + } + + endpoint := fmt.Sprintf("PamProviders/Local/%d/Entries", providerId) + if query != nil { + queryParams := query.toQueryString() + if queryParams != "" { + endpoint = fmt.Sprintf("%s?%s", endpoint, queryParams) + } + } + + keyfactorAPIStruct := &request{ + Method: "GET", + Endpoint: endpoint, + Headers: headers, + Payload: nil, + } + + resp, err := c.sendRequest(keyfactorAPIStruct) + if err != nil { + return nil, err + } + + var jsonResp []LocalPAMEntryResponse + err = json.NewDecoder(resp.Body).Decode(&jsonResp) + if err != nil { + return nil, err + } + return &jsonResp, nil +} + +// CreateLocalPAMEntry creates a new local PAM entry for the given PAM provider +func (c *Client) CreateLocalPAMEntry(providerId int, entry *LocalPAMEntryCreateRequest) ( + *LocalPAMEntryResponse, + error, +) { + log.Printf("[INFO] Creating local PAM entry for provider ID: %d", providerId) + + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + {"Content-Type", "application/json"}, + }, + } + + endpoint := fmt.Sprintf("PamProviders/Local/%d/Entries", providerId) + keyfactorAPIStruct := &request{ + Method: "POST", + Endpoint: endpoint, + Headers: headers, + Payload: entry, + } + + resp, err := c.sendRequest(keyfactorAPIStruct) + if err != nil { + return nil, err + } + + var jsonResp LocalPAMEntryResponse + err = json.NewDecoder(resp.Body).Decode(&jsonResp) + if err != nil { + return nil, err + } + return &jsonResp, nil +} + +// UpdateLocalPAMEntry updates an existing local PAM entry for the given PAM provider +func (c *Client) UpdateLocalPAMEntry( + providerId int, + secretName string, + entry *LocalPAMEntryUpdateRequest, +) (*LocalPAMEntryResponse, error) { + log.Printf("[INFO] Updating local PAM entry '%s' for provider ID: %d", secretName, providerId) + + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + {"Content-Type", "application/json"}, + }, + } + + endpoint := fmt.Sprintf("PamProviders/Local/%d/Entries/%s", providerId, secretName) + keyfactorAPIStruct := &request{ + Method: "PUT", + Endpoint: endpoint, + Headers: headers, + Payload: entry, + } + + resp, err := c.sendRequest(keyfactorAPIStruct) + if err != nil { + return nil, err + } + + var jsonResp LocalPAMEntryResponse + err = json.NewDecoder(resp.Body).Decode(&jsonResp) + if err != nil { + return nil, err + } + return &jsonResp, nil +} + +// DeleteLocalPAMEntry deletes a local PAM entry for the given PAM provider +func (c *Client) DeleteLocalPAMEntry(providerId int, secretName string) error { + log.Printf("[INFO] Deleting local PAM entry '%s' for provider ID: %d", secretName, providerId) + + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + }, + } + + endpoint := fmt.Sprintf("PamProviders/Local/%d/Entries/%s", providerId, secretName) + keyfactorAPIStruct := &request{ + Method: "DELETE", + Endpoint: endpoint, + Headers: headers, + Payload: nil, + } + + _, err := c.sendRequest(keyfactorAPIStruct) + if err != nil { + return err + } + + return nil +} + +// GetPAMProviderQuery represents query parameters for PAM provider listing +type GetPAMProviderQuery struct { + QueryString string + PageReturned int + ReturnLimit int + SortField string + SortAscending int +} + +// toQueryString converts query parameters to URL query string +func (q *GetPAMProviderQuery) toQueryString() string { + if q == nil { + return "" + } + + params := "" + if q.QueryString != "" { + params += fmt.Sprintf("QueryString=%s&", q.QueryString) + } + if q.PageReturned > 0 { + params += fmt.Sprintf("PageReturned=%d&", q.PageReturned) + } + if q.ReturnLimit > 0 { + params += fmt.Sprintf("ReturnLimit=%d&", q.ReturnLimit) + } + if q.SortField != "" { + params += fmt.Sprintf("SortField=%s&", q.SortField) + // Only add SortAscending if SortField is provided + params += fmt.Sprintf("SortAscending=%d&", q.SortAscending) + } + + // Remove trailing '&' + if len(params) > 0 && params[len(params)-1] == '&' { + params = params[:len(params)-1] + } + + return params +} diff --git a/v3/api/pam_test.go b/v3/api/pam_test.go new file mode 100644 index 0000000..a199714 --- /dev/null +++ b/v3/api/pam_test.go @@ -0,0 +1,831 @@ +// Copyright 2025 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestListPAMProviders(t *testing.T) { + tests := []struct { + name string + query *GetPAMProviderQuery + mockResponse interface{} + mockStatusCode int + wantErr bool + wantCount int + }{ + { + name: "successful list without query", + query: nil, + mockResponse: []ProviderResponseLegacy{ + mockProviderResponseLegacy, + }, + mockStatusCode: http.StatusOK, + wantErr: false, + wantCount: 1, + }, + { + name: "successful list with query", + query: &GetPAMProviderQuery{ + QueryString: "Name -eq 'Test'", + ReturnLimit: 10, + PageReturned: 1, + }, + mockResponse: []ProviderResponseLegacy{ + mockProviderResponseLegacy, + }, + mockStatusCode: http.StatusOK, + wantErr: false, + wantCount: 1, + }, + { + name: "empty list", + query: nil, + mockResponse: []ProviderResponseLegacy{}, + mockStatusCode: http.StatusOK, + wantErr: false, + wantCount: 0, + }, + { + name: "server error", + query: nil, + mockResponse: map[string]string{"error": "internal server error"}, + mockStatusCode: http.StatusInternalServerError, + wantErr: true, + wantCount: 0, + }, + } + + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + server := httptest.NewTLSServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + // Verify request path starts with expected endpoint + if !strings.HasPrefix(r.URL.Path, "/KeyfactorAPI/PamProviders") { + t.Errorf("Expected path to start with /KeyfactorAPI/PamProviders, got %s", r.URL.Path) + } + if r.Method != "GET" { + t.Errorf("Expected GET method, got %s", r.Method) + } + + w.WriteHeader(tt.mockStatusCode) + _ = json.NewEncoder(w).Encode(tt.mockResponse) + }, + ), + ) + defer server.Close() + + client := newTestClient(server) + + got, err := client.ListPAMProviders(tt.query) + if (err != nil) != tt.wantErr { + t.Errorf("ListPAMProviders() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && got != nil { + if len(*got) != tt.wantCount { + t.Errorf("ListPAMProviders() got %d items, want %d", len(*got), tt.wantCount) + } + } + }, + ) + } +} + +func TestGetPAMProvider(t *testing.T) { + tests := []struct { + name string + providerId int + mockResponse interface{} + mockStatusCode int + wantErr bool + }{ + { + name: "successful get", + providerId: 1, + mockResponse: mockProviderResponseLegacy, + mockStatusCode: http.StatusOK, + wantErr: false, + }, + { + name: "not found", + providerId: 999, + mockResponse: map[string]string{"error": "not found"}, + mockStatusCode: http.StatusNotFound, + wantErr: true, + }, + { + name: "server error", + providerId: 1, + mockResponse: map[string]string{"error": "internal server error"}, + mockStatusCode: http.StatusInternalServerError, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + server := httptest.NewTLSServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + expectedPath := "/KeyfactorAPI/PamProviders/1" + if tt.providerId == 999 { + expectedPath = "/KeyfactorAPI/PamProviders/999" + } + if r.URL.Path != expectedPath { + t.Errorf("Expected path %s, got %s", expectedPath, r.URL.Path) + } + if r.Method != "GET" { + t.Errorf("Expected GET method, got %s", r.Method) + } + + w.WriteHeader(tt.mockStatusCode) + _ = json.NewEncoder(w).Encode(tt.mockResponse) + }, + ), + ) + defer server.Close() + + client := newTestClient(server) + + got, err := client.GetPAMProvider(tt.providerId) + if (err != nil) != tt.wantErr { + t.Errorf("GetPAMProvider() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && got != nil { + if got.Id != mockProviderResponseLegacy.Id { + t.Errorf("GetPAMProvider() Id = %v, want %v", got.Id, mockProviderResponseLegacy.Id) + } + } + }, + ) + } +} + +func TestCreatePAMProvider(t *testing.T) { + createRequest := &ProviderCreateRequest{ + Name: "New PAM Provider", + Remote: false, + Area: 1, + ProviderType: ProviderType{ + Id: mockProviderTypeId, + Name: &mockProviderTypeName, + }, + SecuredAreaId: intPtr(1), + } + + tests := []struct { + name string + request *ProviderCreateRequest + mockResponse interface{} + mockStatusCode int + wantErr bool + }{ + { + name: "successful create", + request: createRequest, + mockResponse: mockProviderResponseLegacy, + mockStatusCode: http.StatusOK, + wantErr: false, + }, + { + name: "bad request - missing name", + request: &ProviderCreateRequest{}, + mockResponse: map[string]string{"error": "name is required"}, + mockStatusCode: http.StatusBadRequest, + wantErr: true, + }, + { + name: "server error", + request: createRequest, + mockResponse: map[string]string{"error": "internal server error"}, + mockStatusCode: http.StatusInternalServerError, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + server := httptest.NewTLSServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/KeyfactorAPI/PamProviders" { + t.Errorf("Expected path /KeyfactorAPI/PamProviders, got %s", r.URL.Path) + } + if r.Method != "POST" { + t.Errorf("Expected POST method, got %s", r.Method) + } + + // Verify request body + var receivedRequest ProviderCreateRequest + if err := json.NewDecoder(r.Body).Decode(&receivedRequest); err != nil { + t.Errorf("Failed to decode request body: %v", err) + } + if receivedRequest.Name != tt.request.Name { + t.Errorf("Expected name %s, got %s", tt.request.Name, receivedRequest.Name) + } + + w.WriteHeader(tt.mockStatusCode) + _ = json.NewEncoder(w).Encode(tt.mockResponse) + }, + ), + ) + defer server.Close() + + client := newTestClient(server) + + got, err := client.CreatePAMProvider(tt.request) + if (err != nil) != tt.wantErr { + t.Errorf("CreatePAMProvider() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && got != nil { + if got.Id != mockProviderResponseLegacy.Id { + t.Errorf("CreatePAMProvider() Id = %v, want %v", got.Id, mockProviderResponseLegacy.Id) + } + } + }, + ) + } +} + +func TestUpdatePAMProvider(t *testing.T) { + updateRequest := &ProviderUpdateRequestLegacy{ + Id: 1, + Name: "Updated PAM Provider", + Remote: false, + Area: 1, + ProviderType: ProviderType{ + Id: mockProviderTypeId, + Name: &mockProviderTypeName, + }, + SecuredAreaId: intPtr(1), + } + + updatedResponse := ProviderResponseLegacy{ + Id: 1, + Name: stringPtr("Updated PAM Provider"), + Area: 1, + ProviderType: &ProviderType{ + Id: mockProviderTypeId, + Name: &mockProviderTypeName, + }, + SecuredAreaId: intPtr(1), + Remote: false, + } + + tests := []struct { + name string + request *ProviderUpdateRequestLegacy + mockResponse interface{} + mockStatusCode int + wantErr bool + }{ + { + name: "successful update", + request: updateRequest, + mockResponse: updatedResponse, + mockStatusCode: http.StatusOK, + wantErr: false, + }, + { + name: "not found", + request: &ProviderUpdateRequestLegacy{ + Id: 999, + Name: "Nonexistent Provider", + }, + mockResponse: map[string]string{"error": "not found"}, + mockStatusCode: http.StatusNotFound, + wantErr: true, + }, + { + name: "bad request", + request: &ProviderUpdateRequestLegacy{Id: 1}, + mockResponse: map[string]string{"error": "invalid request"}, + mockStatusCode: http.StatusBadRequest, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + server := httptest.NewTLSServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != "/KeyfactorAPI/PamProviders" { + t.Errorf("Expected path /KeyfactorAPI/PamProviders, got %s", r.URL.Path) + } + if r.Method != "PUT" { + t.Errorf("Expected PUT method, got %s", r.Method) + } + + // Verify request body + var receivedRequest ProviderUpdateRequestLegacy + if err := json.NewDecoder(r.Body).Decode(&receivedRequest); err != nil { + t.Errorf("Failed to decode request body: %v", err) + } + if receivedRequest.Id != tt.request.Id { + t.Errorf("Expected Id %d, got %d", tt.request.Id, receivedRequest.Id) + } + + w.WriteHeader(tt.mockStatusCode) + _ = json.NewEncoder(w).Encode(tt.mockResponse) + }, + ), + ) + defer server.Close() + + client := newTestClient(server) + + got, err := client.UpdatePAMProvider(tt.request) + if (err != nil) != tt.wantErr { + t.Errorf("UpdatePAMProvider() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && got != nil { + if *got.Name != "Updated PAM Provider" { + t.Errorf("UpdatePAMProvider() Name = %v, want %v", *got.Name, "Updated PAM Provider") + } + } + }, + ) + } +} + +func TestDeletePAMProvider(t *testing.T) { + tests := []struct { + name string + providerId int + mockStatusCode int + wantErr bool + }{ + { + name: "successful delete", + providerId: 1, + mockStatusCode: http.StatusNoContent, + wantErr: false, + }, + { + name: "not found", + providerId: 999, + mockStatusCode: http.StatusNotFound, + wantErr: true, + }, + { + name: "provider in use", + providerId: 1, + mockStatusCode: http.StatusConflict, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + server := httptest.NewTLSServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + expectedPath := "/KeyfactorAPI/PamProviders/1" + if tt.providerId == 999 { + expectedPath = "/KeyfactorAPI/PamProviders/999" + } + if r.URL.Path != expectedPath { + t.Errorf("Expected path %s, got %s", expectedPath, r.URL.Path) + } + if r.Method != "DELETE" { + t.Errorf("Expected DELETE method, got %s", r.Method) + } + + w.WriteHeader(tt.mockStatusCode) + }, + ), + ) + defer server.Close() + + client := newTestClient(server) + + err := client.DeletePAMProvider(tt.providerId) + if (err != nil) != tt.wantErr { + t.Errorf("DeletePAMProvider() error = %v, wantErr %v", err, tt.wantErr) + } + }, + ) + } +} + +func TestListLocalPAMEntries(t *testing.T) { + tests := []struct { + name string + providerId int + query *GetPAMProviderQuery + mockResponse interface{} + mockStatusCode int + wantErr bool + wantCount int + }{ + { + name: "successful list without query", + providerId: 1, + query: nil, + mockResponse: []LocalPAMEntryResponse{ + mockLocalPAMEntry, + }, + mockStatusCode: http.StatusOK, + wantErr: false, + wantCount: 1, + }, + { + name: "successful list with query", + providerId: 1, + query: &GetPAMProviderQuery{ + ReturnLimit: 10, + }, + mockResponse: []LocalPAMEntryResponse{ + mockLocalPAMEntry, + }, + mockStatusCode: http.StatusOK, + wantErr: false, + wantCount: 1, + }, + { + name: "empty list", + providerId: 1, + query: nil, + mockResponse: []LocalPAMEntryResponse{}, + mockStatusCode: http.StatusOK, + wantErr: false, + wantCount: 0, + }, + { + name: "provider not found", + providerId: 999, + query: nil, + mockResponse: map[string]string{"error": "provider not found"}, + mockStatusCode: http.StatusNotFound, + wantErr: true, + wantCount: 0, + }, + } + + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + server := httptest.NewTLSServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + expectedPath := "/KeyfactorAPI/PamProviders/Local/1/Entries" + if tt.providerId == 999 { + expectedPath = "/KeyfactorAPI/PamProviders/Local/999/Entries" + } + if !strings.HasPrefix(r.URL.Path, expectedPath) { + t.Errorf("Expected path to start with %s, got %s", expectedPath, r.URL.Path) + } + if r.Method != "GET" { + t.Errorf("Expected GET method, got %s", r.Method) + } + + w.WriteHeader(tt.mockStatusCode) + _ = json.NewEncoder(w).Encode(tt.mockResponse) + }, + ), + ) + defer server.Close() + + client := newTestClient(server) + + got, err := client.ListLocalPAMEntries(tt.providerId, tt.query) + if (err != nil) != tt.wantErr { + t.Errorf("ListLocalPAMEntries() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && got != nil { + if len(*got) != tt.wantCount { + t.Errorf("ListLocalPAMEntries() got %d items, want %d", len(*got), tt.wantCount) + } + } + }, + ) + } +} + +func TestCreateLocalPAMEntry(t *testing.T) { + createRequest := &LocalPAMEntryCreateRequest{ + SecretName: "new-secret", + Description: stringPtr("New secret description"), + SecretValue: "super-secret-value", + } + + createdResponse := LocalPAMEntryResponse{ + ProviderId: 1, + SecretName: stringPtr("new-secret"), + Description: stringPtr("New secret description"), + } + + tests := []struct { + name string + providerId int + request *LocalPAMEntryCreateRequest + mockResponse interface{} + mockStatusCode int + wantErr bool + }{ + { + name: "successful create", + providerId: 1, + request: createRequest, + mockResponse: createdResponse, + mockStatusCode: http.StatusOK, + wantErr: false, + }, + { + name: "bad request - missing secret name", + providerId: 1, + request: &LocalPAMEntryCreateRequest{ + SecretValue: "value", + }, + mockResponse: map[string]string{"error": "secret name is required"}, + mockStatusCode: http.StatusBadRequest, + wantErr: true, + }, + { + name: "provider not found", + providerId: 999, + request: createRequest, + mockResponse: map[string]string{"error": "provider not found"}, + mockStatusCode: http.StatusNotFound, + wantErr: true, + }, + { + name: "duplicate secret name", + providerId: 1, + request: createRequest, + mockResponse: map[string]string{"error": "secret already exists"}, + mockStatusCode: http.StatusConflict, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + server := httptest.NewTLSServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + expectedPath := "/KeyfactorAPI/PamProviders/Local/1/Entries" + if tt.providerId == 999 { + expectedPath = "/KeyfactorAPI/PamProviders/Local/999/Entries" + } + if r.URL.Path != expectedPath { + t.Errorf("Expected path %s, got %s", expectedPath, r.URL.Path) + } + if r.Method != "POST" { + t.Errorf("Expected POST method, got %s", r.Method) + } + + // Verify request body + var receivedRequest LocalPAMEntryCreateRequest + if err := json.NewDecoder(r.Body).Decode(&receivedRequest); err != nil { + t.Errorf("Failed to decode request body: %v", err) + } + if receivedRequest.SecretName != tt.request.SecretName { + t.Errorf( + "Expected SecretName %s, got %s", + tt.request.SecretName, + receivedRequest.SecretName, + ) + } + + w.WriteHeader(tt.mockStatusCode) + _ = json.NewEncoder(w).Encode(tt.mockResponse) + }, + ), + ) + defer server.Close() + + client := newTestClient(server) + + got, err := client.CreateLocalPAMEntry(tt.providerId, tt.request) + if (err != nil) != tt.wantErr { + t.Errorf("CreateLocalPAMEntry() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && got != nil { + if *got.SecretName != "new-secret" { + t.Errorf("CreateLocalPAMEntry() SecretName = %v, want %v", *got.SecretName, "new-secret") + } + } + }, + ) + } +} + +func TestUpdateLocalPAMEntry(t *testing.T) { + updateRequest := &LocalPAMEntryUpdateRequest{ + SecretName: "updated-secret", + Description: stringPtr("Updated description"), + SecretValue: stringPtr("updated-value"), + } + + updatedResponse := LocalPAMEntryResponse{ + ProviderId: 1, + SecretName: stringPtr("updated-secret"), + Description: stringPtr("Updated description"), + } + + tests := []struct { + name string + providerId int + secretName string + request *LocalPAMEntryUpdateRequest + mockResponse interface{} + mockStatusCode int + wantErr bool + }{ + { + name: "successful update", + providerId: 1, + secretName: "test-secret", + request: updateRequest, + mockResponse: updatedResponse, + mockStatusCode: http.StatusOK, + wantErr: false, + }, + { + name: "secret not found", + providerId: 1, + secretName: "nonexistent", + request: &LocalPAMEntryUpdateRequest{ + SecretName: "nonexistent", + }, + mockResponse: map[string]string{"error": "secret not found"}, + mockStatusCode: http.StatusNotFound, + wantErr: true, + }, + { + name: "provider not found", + providerId: 999, + secretName: "test-secret", + request: updateRequest, + mockResponse: map[string]string{"error": "provider not found"}, + mockStatusCode: http.StatusNotFound, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + server := httptest.NewTLSServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + expectedPath := "/KeyfactorAPI/PamProviders/Local/1/Entries/test-secret" + if tt.providerId == 999 { + expectedPath = "/KeyfactorAPI/PamProviders/Local/999/Entries/test-secret" + } + if tt.secretName == "nonexistent" { + expectedPath = "/KeyfactorAPI/PamProviders/Local/1/Entries/nonexistent" + } + if r.URL.Path != expectedPath { + t.Errorf("Expected path %s, got %s", expectedPath, r.URL.Path) + } + if r.Method != "PUT" { + t.Errorf("Expected PUT method, got %s", r.Method) + } + + // Verify request body + var receivedRequest LocalPAMEntryUpdateRequest + if err := json.NewDecoder(r.Body).Decode(&receivedRequest); err != nil { + t.Errorf("Failed to decode request body: %v", err) + } + if receivedRequest.SecretName != tt.request.SecretName { + t.Errorf( + "Expected SecretName %s, got %s", + tt.request.SecretName, + receivedRequest.SecretName, + ) + } + + w.WriteHeader(tt.mockStatusCode) + _ = json.NewEncoder(w).Encode(tt.mockResponse) + }, + ), + ) + defer server.Close() + + client := newTestClient(server) + + got, err := client.UpdateLocalPAMEntry(tt.providerId, tt.secretName, tt.request) + if (err != nil) != tt.wantErr { + t.Errorf("UpdateLocalPAMEntry() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !tt.wantErr && got != nil { + if *got.SecretName != "updated-secret" { + t.Errorf("UpdateLocalPAMEntry() SecretName = %v, want %v", *got.SecretName, "updated-secret") + } + } + }, + ) + } +} + +func TestDeleteLocalPAMEntry(t *testing.T) { + tests := []struct { + name string + providerId int + secretName string + mockStatusCode int + wantErr bool + }{ + { + name: "successful delete", + providerId: 1, + secretName: "test-secret", + mockStatusCode: http.StatusNoContent, + wantErr: false, + }, + { + name: "secret not found", + providerId: 1, + secretName: "nonexistent", + mockStatusCode: http.StatusNotFound, + wantErr: true, + }, + { + name: "provider not found", + providerId: 999, + secretName: "test-secret", + mockStatusCode: http.StatusNotFound, + wantErr: true, + }, + { + name: "secret in use", + providerId: 1, + secretName: "in-use-secret", + mockStatusCode: http.StatusConflict, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run( + tt.name, func(t *testing.T) { + server := httptest.NewTLSServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + expectedPath := "/KeyfactorAPI/PamProviders/Local/1/Entries/test-secret" + if tt.providerId == 999 { + expectedPath = "/KeyfactorAPI/PamProviders/Local/999/Entries/test-secret" + } + if tt.secretName == "nonexistent" { + expectedPath = "/KeyfactorAPI/PamProviders/Local/1/Entries/nonexistent" + } + if tt.secretName == "in-use-secret" { + expectedPath = "/KeyfactorAPI/PamProviders/Local/1/Entries/in-use-secret" + } + if r.URL.Path != expectedPath { + t.Errorf("Expected path %s, got %s", expectedPath, r.URL.Path) + } + if r.Method != "DELETE" { + t.Errorf("Expected DELETE method, got %s", r.Method) + } + + w.WriteHeader(tt.mockStatusCode) + }, + ), + ) + defer server.Close() + + client := newTestClient(server) + + err := client.DeleteLocalPAMEntry(tt.providerId, tt.secretName) + if (err != nil) != tt.wantErr { + t.Errorf("DeleteLocalPAMEntry() error = %v, wantErr %v", err, tt.wantErr) + } + }, + ) + } +} From 16e971f4833ccf2ecb9580ab79359e33fbedd58b Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 4 Dec 2025 15:08:21 -0800 Subject: [PATCH 23/51] fix(api/models): PAM `ProviderType` models `Name` to use string rather than pointer --- v3/api/pam_test.go | 6 +++--- v3/api/pam_types.go | 2 +- v3/api/pam_types_models.go | 10 +++++----- v3/api/pam_types_test.go | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/v3/api/pam_test.go b/v3/api/pam_test.go index a199714..5603a97 100644 --- a/v3/api/pam_test.go +++ b/v3/api/pam_test.go @@ -190,7 +190,7 @@ func TestCreatePAMProvider(t *testing.T) { Area: 1, ProviderType: ProviderType{ Id: mockProviderTypeId, - Name: &mockProviderTypeName, + Name: mockProviderTypeName, }, SecuredAreaId: intPtr(1), } @@ -279,7 +279,7 @@ func TestUpdatePAMProvider(t *testing.T) { Area: 1, ProviderType: ProviderType{ Id: mockProviderTypeId, - Name: &mockProviderTypeName, + Name: mockProviderTypeName, }, SecuredAreaId: intPtr(1), } @@ -290,7 +290,7 @@ func TestUpdatePAMProvider(t *testing.T) { Area: 1, ProviderType: &ProviderType{ Id: mockProviderTypeId, - Name: &mockProviderTypeName, + Name: mockProviderTypeName, }, SecuredAreaId: intPtr(1), Remote: false, diff --git a/v3/api/pam_types.go b/v3/api/pam_types.go index 3aa78f7..ad7348c 100644 --- a/v3/api/pam_types.go +++ b/v3/api/pam_types.go @@ -60,7 +60,7 @@ func (c *Client) GetPAMProviderTypeByName(name string) (*ProviderTypeResponse, e // find the provider type with the matching name for _, t := range *types { - if t.Name != nil && *t.Name == name { + if t.Name == name { return &t, nil } } diff --git a/v3/api/pam_types_models.go b/v3/api/pam_types_models.go index d165df0..bcbe0f8 100644 --- a/v3/api/pam_types_models.go +++ b/v3/api/pam_types_models.go @@ -30,7 +30,7 @@ type SecretType int // ProviderTypeParameterResponse represents a parameter for a PAM provider type type ProviderTypeParameterResponse struct { Id int `json:"Id,omitempty"` - Name *string `json:"Name,omitempty"` + Name string `json:"Name,omitempty"` DisplayName *string `json:"DisplayName,omitempty"` DataType PamParameterDataType `json:"DataType,omitempty"` InstanceLevel bool `json:"InstanceLevel,omitempty"` @@ -39,7 +39,7 @@ type ProviderTypeParameterResponse struct { // ProviderTypeResponse represents a PAM provider type type ProviderTypeResponse struct { Id string `json:"Id,omitempty"` // UUID format - Name *string `json:"Name,omitempty"` + Name string `json:"Name,omitempty"` Parameters *[]ProviderTypeParameterResponse `json:"Parameters,omitempty"` } @@ -60,7 +60,7 @@ type ProviderTypeCreateRequest struct { // ProviderCreateRequestProviderTypeParam represents a provider type parameter in a provider creation request type ProviderCreateRequestProviderTypeParam struct { Id int `json:"Id,omitempty"` - Name *string `json:"Name,omitempty"` + Name string `json:"Name,omitempty"` DisplayName *string `json:"DisplayName,omitempty"` InstanceLevel bool `json:"InstanceLevel,omitempty"` } @@ -68,7 +68,7 @@ type ProviderCreateRequestProviderTypeParam struct { // PamProviderTypeParam represents a provider type parameter (full model) for PAM operations type PamProviderTypeParam struct { Id int `json:"Id,omitempty"` - Name *string `json:"Name,omitempty"` + Name string `json:"Name,omitempty"` DisplayName *string `json:"DisplayName,omitempty"` DataType PamParameterDataType `json:"DataType,omitempty"` InstanceLevel bool `json:"InstanceLevel,omitempty"` @@ -78,7 +78,7 @@ type PamProviderTypeParam struct { // ProviderType represents a PAM provider type (full model) type ProviderType struct { Id string `json:"Id,omitempty"` // UUID format - Name *string `json:"Name,omitempty"` + Name string `json:"Name,omitempty"` ProviderTypeParams *[]PamProviderTypeParam `json:"ProviderTypeParams,omitempty"` } diff --git a/v3/api/pam_types_test.go b/v3/api/pam_types_test.go index eac814a..da664aa 100644 --- a/v3/api/pam_types_test.go +++ b/v3/api/pam_types_test.go @@ -87,7 +87,7 @@ var ( Area: 1, ProviderType: &ProviderType{ Id: mockProviderTypeId, - Name: &mockProviderTypeName, + Name: mockProviderTypeName, }, SecuredAreaId: intPtr(1), Remote: false, From 7537550756191ecf79a9eaa89dc919634ba7e575 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Fri, 5 Dec 2025 13:58:43 -0800 Subject: [PATCH 24/51] fix(api/pam): Add `GetPamProviderByName` helper function --- v3/api/pam.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/v3/api/pam.go b/v3/api/pam.go index bb6fb49..37fcd09 100644 --- a/v3/api/pam.go +++ b/v3/api/pam.go @@ -45,6 +45,24 @@ func (c *Client) ListPAMProviders(query *GetPAMProviderQuery) (*[]ProviderRespon return &jsonResp, nil } +// GetPamProviderByName returns a specific PAM provider by name +func (c *Client) GetPamProviderByName(name string) (*ProviderResponseLegacy, error) { + log.Printf("[INFO] Getting PAM provider with name: %s", name) + + query := &GetPAMProviderQuery{ + QueryString: fmt.Sprintf("Name eq '%s'", name), + } + providers, err := c.ListPAMProviders(query) + if err != nil { + return nil, err + } + + if providers == nil || len(*providers) == 0 { + return nil, fmt.Errorf("PAM provider with name '%s' not found", name) + } + return &(*providers)[0], nil +} + // GetPAMProvider returns a specific PAM provider by ID func (c *Client) GetPAMProvider(id int) (*ProviderResponseLegacy, error) { log.Printf("[INFO] Getting PAM provider with ID: %d", id) From 6b3d479cb56e672b0b6bede7a66c9165790ffeb9 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Mon, 15 Dec 2025 09:30:38 -0800 Subject: [PATCH 25/51] fix(api/stores): Use same `StorePasswordConfig` for create and update store models --- v3/api/store_models.go | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/v3/api/store_models.go b/v3/api/store_models.go index bf4db2c..c466842 100644 --- a/v3/api/store_models.go +++ b/v3/api/store_models.go @@ -51,14 +51,14 @@ type UpdateStoreFctArgs struct { // automatically populated by the CreateStore method. However, if configured, this field will be used. PropertiesString string `json:"Properties,omitempty"` // Mapped name-value pair field used to configure properties. - Properties map[string]interface{} `json:"-"` - AgentId string `json:"AgentId"` - AgentAssigned *bool `json:"AgentAssigned,omitempty"` - ContainerName *string `json:"ContainerName,omitempty"` - InventorySchedule *InventorySchedule `json:"InventorySchedule,omitempty"` - ReEnrollmentStatus *ReEnrollmnentConfig `json:"ReEnrollmentStatus,omitempty"` - SetNewPasswordAllowed *bool `json:"SetNewPasswordAllowed,omitempty"` - Password *UpdateStorePasswordConfig `json:"Password"` + Properties map[string]interface{} `json:"-"` + AgentId string `json:"AgentId"` + AgentAssigned *bool `json:"AgentAssigned,omitempty"` + ContainerName *string `json:"ContainerName,omitempty"` + InventorySchedule *InventorySchedule `json:"InventorySchedule,omitempty"` + ReEnrollmentStatus *ReEnrollmnentConfig `json:"ReEnrollmentStatus,omitempty"` + SetNewPasswordAllowed *bool `json:"SetNewPasswordAllowed,omitempty"` + Password *StorePasswordConfig `json:"Password"` } type UpdateStorePasswordConfig struct { @@ -102,14 +102,14 @@ type ReEnrollmnentConfig struct { // StorePasswordConfig configures the password field for a new certificate store. // TODO: make re-usable struct for Secret type fields type StorePasswordConfig struct { - Value *string `json:"SecretValue"` - SecretTypeGuid *string `json:"SecretTypeGuid,omitempty"` - InstanceId *string `json:"InstanceId,omitempty"` - InstanceGuid *string `json:"InstanceGuid,omitempty"` - ProvidererTypeParameterValues *[]ProviderTypeParameterValue `json:"ProviderTypeParameterValues"` - ProviderId int `json:"ProviderId"` - IsManaged bool `json:"IsManaged"` - HasValue bool `json:"HasValue"` + Value *string `json:"SecretValue"` + SecretTypeGuid *string `json:"SecretTypeGuid,omitempty"` + InstanceId *string `json:"InstanceId,omitempty"` + InstanceGuid *string `json:"InstanceGuid,omitempty"` + ProviderTypeParameterValues *[]ProviderTypeParameterValue `json:"ProviderTypeParameterValues"` + ProviderId int `json:"ProviderId"` + IsManaged bool `json:"IsManaged"` + HasValue bool `json:"HasValue"` } // ProviderTypeParameterValues - Not yet implemented // ProviderTypeParameterValues ProviderTypeParams - Not implemented From ba189ad8fc429e37565ce0641ea6c2f00f742a68 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Fri, 9 Jan 2026 13:05:52 -0800 Subject: [PATCH 26/51] fix(models/stores): Use `UpdateStorePasswordConfig` on PUT and POST payloads. feat(models/stores): Add `RemoteProviderName` to `StorePasswordConfig` Signed-off-by: spbsoluble <1661003+spbsoluble@users.noreply.github.com> --- v3/api/store_models.go | 59 +++++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 29 deletions(-) diff --git a/v3/api/store_models.go b/v3/api/store_models.go index c466842..6cbb38c 100644 --- a/v3/api/store_models.go +++ b/v3/api/store_models.go @@ -27,14 +27,14 @@ type CreateStoreFctArgs struct { // automatically populated by the CreateStore method. However, if configured, this field will be used. PropertiesString string `json:"Properties,omitempty"` // Mapped name-value pair field used to configure properties. - Properties map[string]interface{} `json:"-"` - AgentId string `json:"AgentId"` - AgentAssigned *bool `json:"AgentAssigned,omitempty"` - ContainerName *string `json:"ContainerName,omitempty"` - InventorySchedule *InventorySchedule `json:"InventorySchedule,omitempty"` - ReEnrollmentStatus *ReEnrollmnentConfig `json:"ReEnrollmentStatus,omitempty"` - SetNewPasswordAllowed *bool `json:"SetNewPasswordAllowed,omitempty"` - Password *StorePasswordConfig `json:"Password"` + Properties map[string]interface{} `json:"-"` + AgentId string `json:"AgentId"` + AgentAssigned *bool `json:"AgentAssigned,omitempty"` + ContainerName *string `json:"ContainerName,omitempty"` + InventorySchedule *InventorySchedule `json:"InventorySchedule,omitempty"` + ReEnrollmentStatus *ReEnrollmnentConfig `json:"ReEnrollmentStatus,omitempty"` + SetNewPasswordAllowed *bool `json:"SetNewPasswordAllowed,omitempty"` + Password *UpdateStorePasswordConfig `json:"Password"` } // UpdateStoreFctArgs holds the function arguments used for calling the UpdateStore method. @@ -51,20 +51,21 @@ type UpdateStoreFctArgs struct { // automatically populated by the CreateStore method. However, if configured, this field will be used. PropertiesString string `json:"Properties,omitempty"` // Mapped name-value pair field used to configure properties. - Properties map[string]interface{} `json:"-"` - AgentId string `json:"AgentId"` - AgentAssigned *bool `json:"AgentAssigned,omitempty"` - ContainerName *string `json:"ContainerName,omitempty"` - InventorySchedule *InventorySchedule `json:"InventorySchedule,omitempty"` - ReEnrollmentStatus *ReEnrollmnentConfig `json:"ReEnrollmentStatus,omitempty"` - SetNewPasswordAllowed *bool `json:"SetNewPasswordAllowed,omitempty"` - Password *StorePasswordConfig `json:"Password"` + Properties map[string]interface{} `json:"-"` + AgentId string `json:"AgentId"` + AgentAssigned *bool `json:"AgentAssigned,omitempty"` + ContainerName *string `json:"ContainerName,omitempty"` + InventorySchedule *InventorySchedule `json:"InventorySchedule,omitempty"` + ReEnrollmentStatus *ReEnrollmnentConfig `json:"ReEnrollmentStatus,omitempty"` + SetNewPasswordAllowed *bool `json:"SetNewPasswordAllowed,omitempty"` + Password *UpdateStorePasswordConfig `json:"Password"` } type UpdateStorePasswordConfig struct { - SecretValue *string `json:"SecretValue"` // used for setting kf-secret value or No Value (null) - Parameters map[string]string `json:"Parameters"` - Provider int `json:"Provider"` + SecretValue *string `json:"SecretValue,omitempty"` // used for setting kf-secret value or No Value ( + // null) + Parameters map[string]string `json:"Parameters,omitempty"` // used for setting PAM parameters + Provider int `json:"Provider"` // used for setting PAM provider ID } // InventorySchedule holds configuration data for creating an inventory schedule for a certificate store in Keyfactor @@ -102,27 +103,27 @@ type ReEnrollmnentConfig struct { // StorePasswordConfig configures the password field for a new certificate store. // TODO: make re-usable struct for Secret type fields type StorePasswordConfig struct { - Value *string `json:"SecretValue"` + Value *string `json:"Value,omitempty"` // TODO: In a GET response this is just `Value`, but in a POST/PUT this is `SecretValue` + TypedValue *string `json:"TypedValue,omitempty"` SecretTypeGuid *string `json:"SecretTypeGuid,omitempty"` InstanceId *string `json:"InstanceId,omitempty"` InstanceGuid *string `json:"InstanceGuid,omitempty"` - ProviderTypeParameterValues *[]ProviderTypeParameterValue `json:"ProviderTypeParameterValues"` + ProviderTypeParameterValues *[]ProviderTypeParameterValue `json:"ProviderTypeParameterValues,omitempty"` ProviderId int `json:"ProviderId"` IsManaged bool `json:"IsManaged"` HasValue bool `json:"HasValue"` -} // ProviderTypeParameterValues - Not yet implemented -// ProviderTypeParameterValues ProviderTypeParams - Not implemented - -/* Future non-critical functionality */ + RemoteProviderName *string `json:"RemoteProviderName,omitempty"` +} +// ProviderTypeParameterValue - Not yet implemented type ProviderTypeParameterValue struct { Id int `json:"Id"` - Value *string `json:"Value"` + Value *string `json:"Value,omitempty"` ParameterId int `json:"ParameterId"` // defaults always to 0, likely deprecated InstanceId *string `json:"InstanceId"` // defaults null, likely deprecated - InstanceGuid *string `json:"InstanceGuid"` - Provider *string `json:"Provider"` // defaults null, likely deprecated - ProviderTypeParam ProviderTypeParam `json:"ProviderTypeParam"` + InstanceGuid *string `json:"InstanceGuid,omitempty"` + Provider *Provider `json:"Provider,omitempty"` // defaults null, likely deprecated + ProviderTypeParam ProviderTypeParam `json:"ProviderTypeParam,omitempty"` } type ProviderTypeParam struct { From d789bbfcefc6ebe1f599a48e8e824b4bb54a65e9 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Fri, 9 Jan 2026 13:07:40 -0800 Subject: [PATCH 27/51] feat(stores): If unable to deserialize `GetCertificateStoreResponse` return raw JSON response in error if possible. Signed-off-by: spbsoluble <1661003+spbsoluble@users.noreply.github.com> --- v3/api/store.go | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/v3/api/store.go b/v3/api/store.go index 1e37c73..7ff31b1 100644 --- a/v3/api/store.go +++ b/v3/api/store.go @@ -18,6 +18,7 @@ import ( "encoding/json" "errors" "fmt" + "io" "log" "net/http" "strconv" @@ -66,9 +67,9 @@ func (c *Client) CreateStore(ca *CreateStoreFctArgs) (*CreateStoreResponse, erro Payload: &ca, } - resp, err := c.sendRequest(keyfactorAPIStruct) - if err != nil { - return nil, err + resp, respErr := c.sendRequest(keyfactorAPIStruct) + if respErr != nil { + return nil, respErr } jsonResp := &CreateStoreResponse{} @@ -274,17 +275,26 @@ func (c *Client) GetCertificateStoreByID(storeId string) (*GetCertificateStoreRe if err != nil { return nil, err } + defer resp.Body.Close() + + bodyBytes, readErr := io.ReadAll(resp.Body) + if readErr != nil { + return nil, readErr + } jsonResp := &GetCertificateStoreResponse{} - err = json.NewDecoder(resp.Body).Decode(&jsonResp) - if err != nil { - return nil, err + if jErr := json.Unmarshal(bodyBytes, &jsonResp); jErr != nil { + rawJson := make(map[string]interface{}) + if mErr := json.Unmarshal(bodyBytes, &rawJson); mErr != nil { + return nil, fmt.Errorf("error decoding response: %v", mErr) + } + return nil, fmt.Errorf("error decoding response: %v, raw response: %v", jErr, rawJson) } jsonResp.Properties = unmarshalPropertiesString(jsonResp.PropertiesString) return jsonResp, nil } -// GetCertificateStoreByID takes arguments for a certificate store ID to facilitate a call to Keyfactor +// GetCertificateStoreByContainerID takes arguments for a certificate store ID to facilitate a call to Keyfactor // that retrieves a certificate store context. Only the store ID is required. A pointer to a GetStoreByIDResp struct // is returned that contains information on the certificate store. func (c *Client) GetCertificateStoreByContainerID(containerID interface{}) (*[]GetCertificateStoreResponse, error) { @@ -647,3 +657,16 @@ func buildPropertiesInterface(properties map[string]string) interface{} { return propertiesInterface } + +func mapToEscapedJSONString(m map[string]interface{}) (string, error) { + // Convert the map to a byte slice of JSON + jsonBytes, err := json.Marshal(m) + if err != nil { + return "", err + } + + // Escape any special characters in the JSON string + escapedString := string(jsonBytes) + + return escapedString, nil +} From 3826688c35ae0f64da8ca86a8beb95a339492c65 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 28 Jan 2026 10:10:45 -0800 Subject: [PATCH 28/51] chore(helpers): Move helpers related to unpacking API responses to API library --- v3/api/certificate.go | 7 +- v3/api/helpers.go | 277 ++++++++++++++++++++++++++++++++++++++++++ v3/go.mod | 1 + v3/go.sum | 2 + 4 files changed, 285 insertions(+), 2 deletions(-) create mode 100644 v3/api/helpers.go diff --git a/v3/api/certificate.go b/v3/api/certificate.go index bd01c40..b2ac95e 100644 --- a/v3/api/certificate.go +++ b/v3/api/certificate.go @@ -784,10 +784,13 @@ func (c *Client) RecoverCertificate( } log.Println("[DEBUG] RecoverCertificate: Decoding PFX chain") - priv, leaf, chain, pErr := pkcs12.DecodeChain(pfxDer, rca.Password) + priv, leaf, chain, pErr := pkcs12.DecodeChain( + pfxDer, + rca.Password, + ) // TODO: Attempt to parse as PKCS12 because that used to be the "default" export format. if pErr != nil { log.Println("[ERROR] RecoverCertificate: Error decoding PFX chain", pErr.Error()) - return nil, nil, nil, &jsonResp.PFX, pErr + return nil, nil, nil, &jsonResp.PFX, nil //TODO: Don't return error because it's probably actually a PEM } log.Println("[INFO] Recovered certificate successfully") diff --git a/v3/api/helpers.go b/v3/api/helpers.go new file mode 100644 index 0000000..94f4dd7 --- /dev/null +++ b/v3/api/helpers.go @@ -0,0 +1,277 @@ +package api + +import ( + `crypto/ecdsa` + `crypto/sha1` + `crypto/x509` + `encoding/base64` + `encoding/hex` + `encoding/pem` + `fmt` + `reflect` + "crypto/rsa" + "crypto/ed25519" + `github.com/spbsoluble/go-pkcs12` + "github.com/youmark/pkcs8" +) + +// UnpackPEM extracts the private key, certificate, and CA certificates from PEM-formatted data. +// If the private key is encrypted (PKCS#8 encrypted format), it will be decrypted using the provided password. +// +// Parameters: +// - pemData: The PEM data as a string, *string, or []byte (may be base64-encoded) +// - password: The password used for decrypting the encrypted private key +// +// Returns: +// - privateKey: The decrypted private key in PEM format +// - certificate: The leaf certificate in PEM format +// - caCertificates: A slice of CA certificates in PEM format (if any) +// - err: An error that describes why the unpacking failed, if any +func UnpackPEM(pemData interface{}, password string) ( + privateKey, certificate string, + caCertificates []string, + err error, +) { + var pemBytes []byte + + // Convert pemData to []byte + switch v := pemData.(type) { + case string: + pemBytes = []byte(v) + // Try base64 decode + if decoded, decodeErr := base64.StdEncoding.DecodeString(v); decodeErr == nil && len(decoded) > 0 { + pemBytes = decoded + } + case *string: + if v == nil { + err = fmt.Errorf("pemData pointer is nil") + return + } + pemBytes = []byte(*v) + // Try base64 decode + if decoded, decodeErr := base64.StdEncoding.DecodeString(*v); decodeErr == nil && len(decoded) > 0 { + pemBytes = decoded + } + case []byte: + pemBytes = v + // Try base64 decode + if decoded, decodeErr := base64.StdEncoding.DecodeString(string(v)); decodeErr == nil && len(decoded) > 0 { + pemBytes = decoded + } + default: + err = fmt.Errorf("invalid pemData type: expected string, *string, or []byte, got %T", pemData) + return + } + + var certificates []string + var encryptedKeyBlock *pem.Block + + // Parse all PEM blocks + remaining := pemBytes + for { + var block *pem.Block + block, remaining = pem.Decode(remaining) + if block == nil { + break + } + + switch block.Type { + case "CERTIFICATE": + certPEM := string(pem.EncodeToMemory(block)) + certificates = append(certificates, certPEM) + case "ENCRYPTED PRIVATE KEY": + encryptedKeyBlock = block + case "RSA PRIVATE KEY", "EC PRIVATE KEY", "PRIVATE KEY": + // Already unencrypted + privateKey = string(pem.EncodeToMemory(block)) + } + } + + // If we found an encrypted private key, decrypt it + if encryptedKeyBlock != nil && privateKey == "" { + decryptedKey, decryptErr := DecryptPKCS8PrivateKey(encryptedKeyBlock.Bytes, password) + if decryptErr != nil { + err = fmt.Errorf("failed to decrypt private key: %v", decryptErr) + return + } + privateKey = decryptedKey + } + + // Assign certificates: first is leaf, rest are CA chain + if len(certificates) > 0 { + certificate = certificates[0] + if len(certificates) > 1 { + caCertificates = certificates[1:] + } + } + + return privateKey, certificate, caCertificates, nil +} + +// DecryptPKCS8PrivateKey decrypts a PKCS#8 encrypted private key and returns it in PEM format. +// Uses the github.com/youmark/pkcs8 package which supports PBES2 encryption schemes +// including AES-128-CBC, AES-192-CBC, AES-256-CBC, AES-128-GCM, AES-192-GCM, AES-256-GCM. +func DecryptPKCS8PrivateKey(encryptedKey []byte, password string) (string, error) { + // Use the pkcs8 package to parse and decrypt the encrypted PKCS#8 key + // This handles PBES2 encryption with various algorithms + parsedKey, err := pkcs8.ParsePKCS8PrivateKey(encryptedKey, []byte(password)) + if err != nil { + return "", fmt.Errorf("failed to decrypt PKCS#8 key: %v", err) + } + + // Encode the decrypted key back to PEM format + pemBlock, encodeErr := EncodePrivateKey(parsedKey) + if encodeErr != nil { + return "", fmt.Errorf("failed to encode decrypted key: %v", encodeErr) + } + + return string(pem.EncodeToMemory(pemBlock)), nil +} + +// UnpackPkcs12 extracts the private key, certificate, and CA certificates from a PKCS#12/PFX file. +// Parameters: +// - pfxData: The byte slice containing the PKCS#12/PFX file data. +// - password: The password used for decrypting the PKCS#12/PFX file. +// +// Returns: +// - privateKey: The private key extracted from the PFX file, in PEM format. +// - certificate: The certificate extracted from the PFX file, in PEM format. +// - caCertificates: A slice of CA certificates extracted from the PFX file, in PEM format (if any). +// - err: An error that describes why the unpacking failed, if any. +func UnpackPkcs12(pfxData interface{}, password string) ( + privateKey, certificate string, + caCertificates []string, + err error, +) { + // Convert pfxData to []byte, if necessary + var pfxBytes []byte + + switch v := pfxData.(type) { + case string: + // attempt to base64 decode first + pfxBytes = []byte(v) // Convert string to []byte + decoded, decodeErr := base64.StdEncoding.DecodeString(v) + if decodeErr == nil && len(decoded) > 0 { + pfxBytes = decoded + break + } + + case *string: + if v == nil { + err = fmt.Errorf("pfxData pointer is nil") + return + } + // attempt to base64 decode first + pfxBytes = []byte(*v) // Convert *string to []byte + decoded, decodeErr := base64.StdEncoding.DecodeString(*v) + if decodeErr == nil && len(decoded) > 0 { + pfxBytes = decoded + break + } + break + case []byte: + pfxBytes = v + default: + err = fmt.Errorf( + "invalid pfxData type: expected string or []byte, got %s (type %T)", + reflect.ValueOf(pfxData), + pfxData, + ) + return + } + + // Decode the PKCS#12 data + parsedKey, parsedCert, parsedCAs, pkcs12Err := pkcs12.DecodeChain(pfxBytes, password) + if pkcs12Err != nil { + err = fmt.Errorf("failed to decode PKCS#12 data: %v", pkcs12Err) + return + } + + // PEM-encode the private key + privateKeyBlock, keyErr := EncodePrivateKey(parsedKey) + if keyErr != nil { + err = fmt.Errorf("failed to encode private key: %v", keyErr) + return + } + privateKey = string(pem.EncodeToMemory(privateKeyBlock)) + + // PEM-encode the certificate + certificateBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: parsedCert.Raw, + } + certificate = string(pem.EncodeToMemory(certificateBlock)) + + // PEM-encode the CA certificates (if any) + for _, caCert := range parsedCAs { + caCertBlock := &pem.Block{ + Type: "CERTIFICATE", + Bytes: caCert.Raw, + } + caCertificates = append(caCertificates, string(pem.EncodeToMemory(caCertBlock))) + } + + return privateKey, certificate, caCertificates, nil +} + +// EncodePrivateKey determines the type of private key (RSA or ECDSA) and encodes it as a PEM block. +// Parameters: +// - key: The private key to encode. +// +// Returns: +// - pemBlock: The PEM block representation of the private key. +// - err: An error if the private key type is unsupported or invalid. +func EncodePrivateKey(key interface{}) (*pem.Block, error) { + switch k := key.(type) { + case *rsa.PrivateKey: + return &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(k), + }, nil + case *ecdsa.PrivateKey: + encodedKey, err := x509.MarshalECPrivateKey(k) + if err != nil { + return nil, fmt.Errorf("failed to encode ECDSA private key: %v", err) + } + return &pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: encodedKey, + }, nil + case ed25519.PrivateKey: + return &pem.Block{ + Type: "PRIVATE KEY", + Bytes: k, + }, nil + default: + return nil, fmt.Errorf("unsupported private key type: %T", key) + } +} + +// GetCertificateThumbprint computes the thumbprint (SHA-1 hash) of an x509 certificate. +// +// The thumbprint is calculated by hashing the raw DER-encoded certificate data +// using the SHA-1 algorithm. +// +// Parameters: +// - cert: A pointer to an x509.Certificate object. +// +// Returns: +// - A string representing the hexadecimal-encoded thumbprint of the certificate. +// - An error, which will be nil if the computation succeeds. +// +// Example: +// +// thumbprint, err := GetCertificateThumbprint(cert) +// if err != nil { +// log.Fatalf("error computing thumbprint: %v", err) +// } +// fmt.Println("Certificate Thumbprint:", thumbprint) +func GetCertificateThumbprint(cert *x509.Certificate) (string, error) { + // Compute the SHA-1 hash of the certificate's raw DER data + hash := sha1.Sum(cert.Raw) + + // Convert the hash to a hexadecimal string + thumbprint := hex.EncodeToString(hash[:]) + + return thumbprint, nil +} diff --git a/v3/go.mod b/v3/go.mod index 70dc405..2cd90a7 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -22,6 +22,7 @@ require ( github.com/Keyfactor/keyfactor-auth-client-go v1.3.0 github.com/hashicorp/terraform-plugin-log v0.10.0 github.com/spbsoluble/go-pkcs12 v0.3.3 + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 go.mozilla.org/pkcs7 v0.9.0 ) diff --git a/v3/go.sum b/v3/go.sum index ede870e..0a2509a 100644 --- a/v3/go.sum +++ b/v3/go.sum @@ -62,6 +62,8 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= go.mozilla.org/pkcs7 v0.9.0 h1:yM4/HS9dYv7ri2biPtxt8ikvB37a980dg69/pKmS+eI= go.mozilla.org/pkcs7 v0.9.0/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= From 1f01c2b470f8b6ea3c82363d957d71979d10bae2 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 28 Jan 2026 10:13:29 -0800 Subject: [PATCH 29/51] feat(helpers): Add common helpers around decoding certificate API responses. --- v3/api/helpers.go | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/v3/api/helpers.go b/v3/api/helpers.go index 94f4dd7..8a2d460 100644 --- a/v3/api/helpers.go +++ b/v3/api/helpers.go @@ -1,18 +1,18 @@ package api import ( - `crypto/ecdsa` - `crypto/sha1` - `crypto/x509` - `encoding/base64` - `encoding/hex` - `encoding/pem` - `fmt` - `reflect` - "crypto/rsa" + "crypto/ecdsa" "crypto/ed25519" - `github.com/spbsoluble/go-pkcs12` + "crypto/rsa" + "crypto/sha1" + "crypto/x509" + "encoding/base64" + "encoding/hex" + "encoding/pem" + "fmt" + "github.com/spbsoluble/go-pkcs12" "github.com/youmark/pkcs8" + "reflect" ) // UnpackPEM extracts the private key, certificate, and CA certificates from PEM-formatted data. From d9eeec6afd6e205cdd09da2e5f83b8de3393b230 Mon Sep 17 00:00:00 2001 From: spb <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 31 Mar 2026 11:35:21 -0700 Subject: [PATCH 30/51] Ab#82568 (#51) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary This PR adds several new API capabilities and bug fixes targeting Keyfactor Command v25+: - **Applications API** — Full CRUD support (`List`, `Get`, `Create`, `Update`, `Delete`) for the `/Applications` endpoint, including all schedule types and backwards compatibility for Command versions prior to v25 - **PAM Providers & Types** — Full CRUD for `/PamProviders` and `/PamProviders/Types`, with a `GetPamProviderByName` helper; model fixes for `ProviderType.Name` and store `Password` field types - **Enrollment Patterns** — Full CRUD for `/EnrollmentPattern`, with new model fields; PFX enrollments can now specify `EnrollmentPatternId` or `Template` (rather than requiring both) - **Certificate enhancements** — New fields on `GetCertificateResponse` (owner role, alt key info, curve, etc.), CSR enrollment args expanded, base64 response from `DownloadCertificate`, `findLeafCert` helper, and graceful handling of ed448 keys - **Store improvements** — Immediate inventory scheduling, `PUT` method capitalization fix, improved error messaging when deserializing store responses, password config model alignment between create/update - **Store types** — Paginate `ListStoreTypes` to avoid truncation on large deployments --------- Co-authored-by: Claude Opus 4.6 --- v3/api/application.go | 232 +++++++++++++++++++++++++++++++++++ v3/api/application_models.go | 96 +++++++++++++++ v3/api/certificate.go | 45 +++++-- v3/api/client.go | 8 ++ v3/api/helpers.go | 7 ++ v3/api/pam_types_test.go | 10 +- v3/api/store.go | 43 ++++++- v3/api/store_container.go | 93 ++++++++++++++ v3/api/store_models.go | 4 +- v3/api/store_type.go | 54 +++++--- 10 files changed, 559 insertions(+), 33 deletions(-) create mode 100644 v3/api/application.go create mode 100644 v3/api/application_models.go diff --git a/v3/api/application.go b/v3/api/application.go new file mode 100644 index 0000000..d8002ad --- /dev/null +++ b/v3/api/application.go @@ -0,0 +1,232 @@ +// Copyright 2024 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "encoding/json" + "fmt" + "log" + "strconv" + "strings" +) + +// isLegacyContainerAPI returns true when the server is pre-v25 and uses +// CertificateStoreContainers instead of the v25+ Applications endpoint. +// Both endpoints accept and return the same JSON schedule format. +func (c *Client) isLegacyContainerAPI() bool { + v := c.AuthClient.GetCommandVersion() + major := commandVersionMajor(v) + return major > 0 && major < 25 +} + +// commandVersionMajor parses the major version number from a product version +// string such as "24.4.0.0" or "25.1.0.0". Returns 0 if unparseable. +func commandVersionMajor(version string) int { + if version == "" { + return 0 + } + parts := strings.SplitN(version, ".", 2) + major, err := strconv.Atoi(parts[0]) + if err != nil { + return 0 + } + return major +} + +// appEndpoint returns the base API endpoint for the connected Command version. +// Pre-v25 uses CertificateStoreContainers; v25+ uses Applications. +// Both endpoints share the same JSON request/response format. +func (c *Client) appEndpoint() string { + if c.isLegacyContainerAPI() { + return "CertificateStoreContainers" + } + return "Applications" +} + +// ListApplications returns all applications/containers. +func (c *Client) ListApplications() ([]ApplicationListItem, error) { + log.Println("[INFO] Listing applications.") + + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + }, + } + + req := &request{ + Method: "GET", + Endpoint: c.appEndpoint(), + Headers: headers, + } + + resp, err := c.sendRequest(req) + if err != nil { + return nil, err + } + + var result []ApplicationListItem + if err = json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, err + } + return result, nil +} + +// GetApplication returns the full details of an application/container by integer ID. +func (c *Client) GetApplication(id int) (*ApplicationResponse, error) { + log.Printf("[INFO] Fetching application with ID %d.", id) + + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + }, + } + + req := &request{ + Method: "GET", + Endpoint: fmt.Sprintf("%s/%d", c.appEndpoint(), id), + Headers: headers, + } + + resp, err := c.sendRequest(req) + if err != nil { + return nil, err + } + + var result ApplicationResponse + if err = json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, err + } + return &result, nil +} + +// GetApplicationByName returns the application matching the given name by +// listing all applications and then fetching the matching one by ID. +// Returns an error if no application with that name exists. +func (c *Client) GetApplicationByName(name string) (*ApplicationResponse, error) { + log.Printf("[INFO] Fetching application with name %q.", name) + + apps, err := c.ListApplications() + if err != nil { + return nil, err + } + + for _, app := range apps { + if app.Name == name { + return c.GetApplication(app.Id) + } + } + return nil, fmt.Errorf("application %q not found", name) +} + +// CreateApplication creates a new application/container and returns the created resource. +func (c *Client) CreateApplication(createReq *ApplicationCreateRequest) (*ApplicationResponse, error) { + log.Println("[INFO] Creating application.") + + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + {"Content-Type", "application/json"}, + }, + } + + req := &request{ + Method: "POST", + Endpoint: c.appEndpoint(), + Headers: headers, + Payload: createReq, + } + + resp, err := c.sendRequest(req) + if err != nil { + return nil, err + } + + var result ApplicationResponse + if err = json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, err + } + return &result, nil +} + +// UpdateApplication performs a full replacement of an existing application/container. +// For v25+ the API uses PUT /Applications with the ID in the body. +// For pre-v25 the API uses PUT /CertificateStoreContainers/{id} with the ID in the path. +func (c *Client) UpdateApplication(id int, updateReq *ApplicationUpdateRequest) (*ApplicationResponse, error) { + log.Printf("[INFO] Updating application with ID %d.", id) + + updateReq.Id = id + + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + {"Content-Type", "application/json"}, + }, + } + + // Both pre-v25 and v25+ use PUT /{endpoint} with the ID in the request body. + endpoint := c.appEndpoint() + + req := &request{ + Method: "PUT", + Endpoint: endpoint, + Headers: headers, + Payload: updateReq, + } + + resp, err := c.sendRequest(req) + if err != nil { + return nil, err + } + + var result ApplicationResponse + if err = json.NewDecoder(resp.Body).Decode(&result); err != nil { + return nil, err + } + return &result, nil +} + +// DeleteApplication deletes an application/container by its integer ID. +// The server returns 204 No Content on success. +func (c *Client) DeleteApplication(id int) error { + log.Printf("[INFO] Deleting application with ID %d.", id) + + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + }, + } + + req := &request{ + Method: "DELETE", + Endpoint: fmt.Sprintf("%s/%d", c.appEndpoint(), id), + Headers: headers, + } + + resp, err := c.sendRequest(req) + if err != nil { + return err + } + + if resp.StatusCode < 200 || resp.StatusCode >= 300 { + return fmt.Errorf("failed to delete application: HTTP %d", resp.StatusCode) + } + + return nil +} diff --git a/v3/api/application_models.go b/v3/api/application_models.go new file mode 100644 index 0000000..63e1bac --- /dev/null +++ b/v3/api/application_models.go @@ -0,0 +1,96 @@ +// Copyright 2024 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +// ApplicationScheduleInterval defines an interval-based inventory schedule. +type ApplicationScheduleInterval struct { + Minutes int `json:"Minutes"` +} + +// ApplicationScheduleDaily defines a daily time-based inventory schedule. +// Also reused as the shape for ExactlyOnce. +type ApplicationScheduleDaily struct { + Time string `json:"Time"` // ISO 8601 datetime string (e.g. "2023-11-25T23:30:00Z") +} + +// ApplicationScheduleWeekly defines a weekly inventory schedule. +// Days are weekday names ("Sunday"…"Saturday"); Time is an ISO 8601 UTC datetime. +type ApplicationScheduleWeekly struct { + Days []string `json:"Days"` // e.g. ["Monday", "Wednesday"] + Time string `json:"Time"` // ISO 8601 datetime string +} + +// ApplicationScheduleMonthly defines a monthly inventory schedule. +// Day is the day-of-month (1–31); Time is an ISO 8601 UTC datetime. +type ApplicationScheduleMonthly struct { + Day int `json:"Day"` + Time string `json:"Time"` // ISO 8601 datetime string +} + +// ApplicationSchedule holds the schedule configuration for an application. +// Set exactly one field; omit all to disable the schedule (Off). +// +// - Immediate: run once immediately (server may convert to ExactlyOnce on next read) +// - Interval: run every N minutes +// - Daily: run at the same time each day +// - Weekly: run on specific weekdays at a given time +// - Monthly: run on a specific day of each month at a given time +// - ExactlyOnce: run exactly once at the specified time +type ApplicationSchedule struct { + Immediate *bool `json:"Immediate,omitempty"` + Interval *ApplicationScheduleInterval `json:"Interval,omitempty"` + Daily *ApplicationScheduleDaily `json:"Daily,omitempty"` + Weekly *ApplicationScheduleWeekly `json:"Weekly,omitempty"` + Monthly *ApplicationScheduleMonthly `json:"Monthly,omitempty"` + ExactlyOnce *ApplicationScheduleDaily `json:"ExactlyOnce,omitempty"` +} + +// ApplicationCertStore is a minimal certificate store reference within an application detail response. +type ApplicationCertStore struct { + Id string `json:"Id"` // Store GUID (UUID) +} + +// ApplicationListItem represents one entry returned by GET /Applications (list endpoint). +// The Schedule field is returned as a cron expression string by the list endpoint. +type ApplicationListItem struct { + Id int `json:"Id"` + Name string `json:"Name"` + Schedule string `json:"Schedule"` +} + +// ApplicationResponse is the full application detail returned by GET /Applications/{id}. +type ApplicationResponse struct { + Id int `json:"Id"` + Name string `json:"Name"` + OverwriteSchedules bool `json:"OverwriteSchedules"` + Schedule *ApplicationSchedule `json:"Schedule,omitempty"` + CertificateStores []ApplicationCertStore `json:"CertificateStores,omitempty"` +} + +// ApplicationCreateRequest is the request body for POST /Applications. +type ApplicationCreateRequest struct { + Name string `json:"Name"` + OverwriteSchedules bool `json:"OverwriteSchedules"` + Schedule *ApplicationSchedule `json:"Schedule,omitempty"` +} + +// ApplicationUpdateRequest is the request body for PUT /Applications/{id}. +// The Id field is set automatically by UpdateApplication. +type ApplicationUpdateRequest struct { + Id int `json:"Id"` + Name string `json:"Name"` + OverwriteSchedules bool `json:"OverwriteSchedules"` + Schedule *ApplicationSchedule `json:"Schedule,omitempty"` +} diff --git a/v3/api/certificate.go b/v3/api/certificate.go index b2ac95e..8e281c2 100644 --- a/v3/api/certificate.go +++ b/v3/api/certificate.go @@ -299,27 +299,58 @@ func (c *Client) DownloadCertificate( return nil, nil, &jsonResp.Content, p7bErr } - var leaf *x509.Certificate + leaf := findLeafCert(certs) if len(certs) > 1 { - //leaf is last cert in chain - leaf = certs[0] // First cert in chain is the leaf return leaf, certs, &jsonResp.Content, nil } + return leaf, nil, &jsonResp.Content, nil +} + +// findLeafCert returns the end-entity (leaf) certificate from a set of +// certificates. It identifies the leaf as the cert whose Subject is not used +// as an Issuer by any other cert in the set — i.e. nothing is signed by it. +// This is order-independent and handles both root-first and leaf-first P7Bs. +// +// When the set contains only one cert, or when the algorithm cannot determine +// a unique leaf (e.g. all certs are self-signed), certs[0] is returned as a +// safe fallback. +func findLeafCert(certs []*x509.Certificate) *x509.Certificate { + if len(certs) == 0 { + return nil + } + if len(certs) == 1 { + return certs[0] + } - return certs[0], nil, &jsonResp.Content, nil + // Build a set of all RawIssuer values (subjects that issued something). + issuers := make(map[string]bool, len(certs)) + for _, c := range certs { + issuers[string(c.RawIssuer)] = true + } + + // The leaf's Subject is not in the issuers set. + for _, c := range certs { + if !issuers[string(c.RawSubject)] { + return c + } + } + + // Fallback: cannot distinguish (e.g. single self-signed cert in multi-cert set). + return certs[0] } // EnrollCSR takes arguments for EnrollCSRFctArgs to enroll a passed Certificate Signing // Request with Keyfactor. An EnrollResponse containing a signed certificate is returned upon successful // enrollment. Required fields to complete a CSR enrollment are: // - CSR : string -// - Template : string +// - Template : string (or EnrollmentPatternId on Command v25+) // - CertificateAuthority : string func (c *Client) EnrollCSR(ea *EnrollCSRFctArgs) (*EnrollResponse, error) { log.Println("[INFO] Signing CSR with Keyfactor") - /* Ensure required inputs exist */ - if (ea.Template == "") || (ea.CertificateAuthority == "") { + /* Ensure required inputs exist. + On Command v25+ an EnrollmentPatternId can substitute for Template. */ + if (ea.Template == "" && ea.EnrollmentPatternId == 0) || (ea.CertificateAuthority == "") { return nil, errors.New("invalid or nonexistent values required for csr enrollment") } diff --git a/v3/api/client.go b/v3/api/client.go index 7c7c136..add6a21 100644 --- a/v3/api/client.go +++ b/v3/api/client.go @@ -124,6 +124,14 @@ type AuthConfig interface { Authenticate() error GetHttpClient() (*http.Client, error) GetServerConfig() *auth_providers.Server + GetCommandVersion() string +} + +// NewKeyfactorClientWithAuth creates a Client with a pre-built AuthConfig, bypassing +// the Authenticate() network call. Used in unit tests with VCR cassettes. +func NewKeyfactorClientWithAuth(auth AuthConfig, ctx *context.Context) *Client { + initLogger(ctx) + return &Client{AuthClient: auth} } // NewKeyfactorClient creates a new Keyfactor client instance. A configured Client is returned with methods used to diff --git a/v3/api/helpers.go b/v3/api/helpers.go index 8a2d460..c57f590 100644 --- a/v3/api/helpers.go +++ b/v3/api/helpers.go @@ -242,6 +242,13 @@ func EncodePrivateKey(key interface{}) (*pem.Block, error) { Type: "PRIVATE KEY", Bytes: k, }, nil + case *pkcs12.OpaquePrivateKey: + // Algorithm not supported by Go's x509 (e.g. Ed448, OID 1.3.101.113). + // The DER is already valid PKCS#8; wrap it directly. + return &pem.Block{ + Type: "PRIVATE KEY", + Bytes: k.DER, + }, nil default: return nil, fmt.Errorf("unsupported private key type: %T", key) } diff --git a/v3/api/pam_types_test.go b/v3/api/pam_types_test.go index da664aa..e5b1d8f 100644 --- a/v3/api/pam_types_test.go +++ b/v3/api/pam_types_test.go @@ -41,6 +41,10 @@ func (m *mockAuthConfig) Authenticate() error { return nil } +func (m *mockAuthConfig) GetCommandVersion() string { + return "25.1.0.0" +} + // newTestClient creates a test client with mock server func newTestClient(server *httptest.Server) *Client { return &Client{ @@ -62,18 +66,18 @@ var ( mockProviderTypeResponse = ProviderTypeResponse{ Id: mockProviderTypeId, - Name: &mockProviderTypeName, + Name: mockProviderTypeName, Parameters: &[]ProviderTypeParameterResponse{ { Id: 1, - Name: stringPtr("Username"), + Name: "Username", DisplayName: stringPtr("User Name"), DataType: PamParameterDataTypeString, InstanceLevel: false, }, { Id: 2, - Name: stringPtr("Password"), + Name: "Password", DisplayName: stringPtr("Password"), DataType: PamParameterDataTypeSecret, InstanceLevel: true, diff --git a/v3/api/store.go b/v3/api/store.go index 7ff31b1..9baf9f7 100644 --- a/v3/api/store.go +++ b/v3/api/store.go @@ -117,7 +117,7 @@ func (c *Client) UpdateStore(ua *UpdateStoreFctArgs) (*UpdateStoreResponse, erro } keyfactorAPIStruct := &request{ - Method: "Put", + Method: "PUT", Endpoint: "CertificateStores", Headers: headers, Payload: &ua, @@ -614,6 +614,47 @@ func unmarshalPropertiesString(properties string) map[string]interface{} { return make(map[string]interface{}) } +// ScheduleImmediateInventory triggers an immediate inventory job on the given certificate store. +// If the store already has any inventory schedule configured this is a no-op. +// When no schedule is present (common for newly-created stores that have never had inventory run) +// it sends a PUT with InventorySchedule.Immediate=true so the orchestrator runs inventory on its +// next check-in and updates Command's inventory record. +// +// Password and server-credential Properties (ServerUsername/Password/UseSsl) are not included in +// the PUT body because they are write-only and are not returned by the GET endpoint. With the +// Password field now tagged json:"Password,omitempty", nil is omitted from JSON entirely so +// Command does not receive a null and will preserve whatever password is already configured. +func (c *Client) ScheduleImmediateInventory(storeId string) error { + storeResp, err := c.GetCertificateStoreByID(storeId) + if err != nil { + return fmt.Errorf("ScheduleImmediateInventory: could not read store %s: %w", storeId, err) + } + + // No-op if any schedule is already configured. + sched := storeResp.InventorySchedule + if sched.Immediate != nil || sched.Interval != nil || sched.Daily != nil || sched.ExactlyOnce != nil { + return nil + } + + immediate := true + _, err = c.UpdateStore(&UpdateStoreFctArgs{ + Id: storeResp.Id, + ClientMachine: storeResp.ClientMachine, + StorePath: storeResp.StorePath, + CertStoreType: storeResp.CertStoreType, + AgentId: storeResp.AgentId, + // Password intentionally nil (omitted from JSON via omitempty) — preserves existing password. + // PropertiesString intentionally empty (omitted from JSON via omitempty) — preserves existing properties. + InventorySchedule: &InventorySchedule{ + Immediate: &immediate, + }, + }) + if err != nil { + return fmt.Errorf("ScheduleImmediateInventory: UpdateStore failed for store %s: %w", storeId, err) + } + return nil +} + func validateCreateStoreArgs(ca *CreateStoreFctArgs) error { if ca.ClientMachine == "" { return errors.New("client machine is required for creation of new certificate store") diff --git a/v3/api/store_container.go b/v3/api/store_container.go index 1a7dcdf..74ec154 100644 --- a/v3/api/store_container.go +++ b/v3/api/store_container.go @@ -15,6 +15,7 @@ package api import ( + "bytes" "encoding/json" "fmt" "log" @@ -128,3 +129,95 @@ func (c *Client) GetStoreContainer(id interface{}) (*CertStoreContainer, error) } return nil, fmt.Errorf("invalid API response from Keyfactor while getting cert store container %s", id) } + +// CreateStoreContainer creates a new certificate store container (pre-v25 legacy API). +func (c *Client) CreateStoreContainer(req *CertStoreContainer) (*CertStoreContainer, error) { + log.Println("[INFO] Creating certificate store container.") + + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + }, + } + + payload, err := json.Marshal(req) + if err != nil { + return nil, err + } + + keyfactorAPIStruct := &request{ + Method: "POST", + Endpoint: "CertificateStoreContainers", + Headers: headers, + Payload: bytes.NewReader(payload), + } + + resp, err := c.sendRequest(keyfactorAPIStruct) + if err != nil { + return nil, err + } + + jsonResp := &CertStoreContainer{} + if err = json.NewDecoder(resp.Body).Decode(jsonResp); err != nil { + return nil, err + } + return jsonResp, nil +} + +// UpdateStoreContainer updates an existing certificate store container (pre-v25 legacy API). +func (c *Client) UpdateStoreContainer(id int, req *CertStoreContainer) (*CertStoreContainer, error) { + log.Printf("[INFO] Updating certificate store container %d.\n", id) + + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + }, + } + + req.Id = &id + payload, err := json.Marshal(req) + if err != nil { + return nil, err + } + + keyfactorAPIStruct := &request{ + Method: "PUT", + Endpoint: fmt.Sprintf("CertificateStoreContainers/%d", id), + Headers: headers, + Payload: bytes.NewReader(payload), + } + + resp, err := c.sendRequest(keyfactorAPIStruct) + if err != nil { + return nil, err + } + + jsonResp := &CertStoreContainer{} + if err = json.NewDecoder(resp.Body).Decode(jsonResp); err != nil { + return nil, err + } + return jsonResp, nil +} + +// DeleteStoreContainer deletes a certificate store container by ID (pre-v25 legacy API). +func (c *Client) DeleteStoreContainer(id int) error { + log.Printf("[INFO] Deleting certificate store container %d.\n", id) + + headers := &apiHeaders{ + Headers: []StringTuple{ + {"x-keyfactor-api-version", "1"}, + {"x-keyfactor-requested-with", "APIClient"}, + }, + } + + keyfactorAPIStruct := &request{ + Method: "DELETE", + Endpoint: fmt.Sprintf("CertificateStoreContainers/%d", id), + Headers: headers, + } + + _, err := c.sendRequest(keyfactorAPIStruct) + return err +} diff --git a/v3/api/store_models.go b/v3/api/store_models.go index 6cbb38c..0486fea 100644 --- a/v3/api/store_models.go +++ b/v3/api/store_models.go @@ -34,7 +34,7 @@ type CreateStoreFctArgs struct { InventorySchedule *InventorySchedule `json:"InventorySchedule,omitempty"` ReEnrollmentStatus *ReEnrollmnentConfig `json:"ReEnrollmentStatus,omitempty"` SetNewPasswordAllowed *bool `json:"SetNewPasswordAllowed,omitempty"` - Password *UpdateStorePasswordConfig `json:"Password"` + Password *UpdateStorePasswordConfig `json:"Password,omitempty"` } // UpdateStoreFctArgs holds the function arguments used for calling the UpdateStore method. @@ -58,7 +58,7 @@ type UpdateStoreFctArgs struct { InventorySchedule *InventorySchedule `json:"InventorySchedule,omitempty"` ReEnrollmentStatus *ReEnrollmnentConfig `json:"ReEnrollmentStatus,omitempty"` SetNewPasswordAllowed *bool `json:"SetNewPasswordAllowed,omitempty"` - Password *UpdateStorePasswordConfig `json:"Password"` + Password *UpdateStorePasswordConfig `json:"Password,omitempty"` } type UpdateStorePasswordConfig struct { diff --git a/v3/api/store_type.go b/v3/api/store_type.go index 94dff94..15032c0 100644 --- a/v3/api/store_type.go +++ b/v3/api/store_type.go @@ -19,6 +19,7 @@ import ( "errors" "fmt" "log" + "strconv" ) // GetCertificateStoreType takes arguments for a certificate store type ID or name and if found will return the certificate store type @@ -101,9 +102,9 @@ func (c *Client) GetCertificateStoreTypeById(id int) (*CertificateStoreType, err return &jsonResp, nil } -// ListCertificateStoreTypes takes no arguments and returns a list of certificate store types from Keyfactor. +// ListCertificateStoreTypes returns all certificate store types from Keyfactor, paginating +// automatically using pq.pageReturned / pq.returnLimit until all results are fetched. func (c *Client) ListCertificateStoreTypes() (*[]CertificateStoreType, error) { - // Set Keyfactor-specific headers headers := &apiHeaders{ Headers: []StringTuple{ {"x-keyfactor-api-version", "1"}, @@ -111,25 +112,38 @@ func (c *Client) ListCertificateStoreTypes() (*[]CertificateStoreType, error) { }, } - endpoint := "CertificateStoreTypes" - keyfactorAPIStruct := &request{ - Method: "GET", - Endpoint: endpoint, - Headers: headers, - Payload: nil, - } - - resp, err := c.sendRequest(keyfactorAPIStruct) - if err != nil { - return nil, err - } - - var jsonResp []CertificateStoreType - err = json.NewDecoder(resp.Body).Decode(&jsonResp) - if err != nil { - return nil, err + const pageSize = 100 + var all []CertificateStoreType + for page := 1; ; page++ { + keyfactorAPIStruct := &request{ + Method: "GET", + Endpoint: "CertificateStoreTypes", + Headers: headers, + Payload: nil, + Query: &apiQuery{ + Query: []StringTuple{ + {"PageReturned", strconv.Itoa(page)}, + {"ReturnLimit", strconv.Itoa(pageSize)}, + }, + }, + } + + resp, err := c.sendRequest(keyfactorAPIStruct) + if err != nil { + return nil, err + } + + var pageResults []CertificateStoreType + err = json.NewDecoder(resp.Body).Decode(&pageResults) + if err != nil { + return nil, err + } + all = append(all, pageResults...) + if len(pageResults) < pageSize { + break + } } - return &jsonResp, nil + return &all, nil } // CreateStoreType takes arguments for CreateStoreFctArgs to facilitate the creation From 8136605d5e7ad348462128dd9aef1a125e5a96bf Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 31 Mar 2026 19:59:08 -0700 Subject: [PATCH 31/51] chore(deps): Bump `keyfactor-auth-client-go v1.5.0` --- v3/go.mod | 21 +++++++++----- v3/go.sum | 85 ++++++++++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 85 insertions(+), 21 deletions(-) diff --git a/v3/go.mod b/v3/go.mod index 2cd90a7..d64106f 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -19,7 +19,7 @@ go 1.24.0 toolchain go1.24.5 require ( - github.com/Keyfactor/keyfactor-auth-client-go v1.3.0 + github.com/Keyfactor/keyfactor-auth-client-go v1.5.0 github.com/hashicorp/terraform-plugin-log v0.10.0 github.com/spbsoluble/go-pkcs12 v0.3.3 github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 @@ -27,7 +27,7 @@ require ( ) require ( - github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 // indirect github.com/Azure/azure-sdk-for-go/sdk/internal v1.11.2 // indirect github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.4.0 // indirect @@ -37,14 +37,21 @@ require ( github.com/golang-jwt/jwt/v5 v5.3.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/go-uuid v1.0.3 // indirect + github.com/jcmturner/aescts/v2 v2.0.0 // indirect + github.com/jcmturner/dnsutils/v2 v2.0.0 // indirect + github.com/jcmturner/gofork v1.7.6 // indirect + github.com/jcmturner/goidentity/v6 v6.0.1 // indirect + github.com/jcmturner/gokrb5/v8 v8.4.4 // indirect + github.com/jcmturner/rpc/v2 v2.0.3 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect - golang.org/x/crypto v0.45.0 // indirect - golang.org/x/net v0.47.0 // indirect - golang.org/x/oauth2 v0.33.0 // indirect - golang.org/x/sys v0.38.0 // indirect - golang.org/x/text v0.31.0 // indirect + golang.org/x/crypto v0.47.0 // indirect + golang.org/x/net v0.49.0 // indirect + golang.org/x/oauth2 v0.34.0 // indirect + golang.org/x/sys v0.40.0 // indirect + golang.org/x/text v0.33.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/v3/go.sum b/v3/go.sum index 0a2509a..e330923 100644 --- a/v3/go.sum +++ b/v3/go.sum @@ -1,5 +1,5 @@ -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0 h1:JXg2dwJUmPB9JmtVmdEB16APJ7jurfbY5jnfXpJoRMc= -github.com/Azure/azure-sdk-for-go/sdk/azcore v1.20.0/go.mod h1:YD5h/ldMsG0XiIw7PdyNhLxaM317eFh5yNLccNfGdyw= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0 h1:fou+2+WFTib47nS+nz/ozhEBnvU96bKHy6LjRsY4E28= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.21.0/go.mod h1:t76Ruy8AHvUAC8GfMWJMa0ElSbuIcO03NLpynfbgsPA= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1 h1:Hk5QBxZQC1jb2Fwj6mpzme37xbCDdNTxU7O9eb5+LB4= github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1/go.mod h1:IYus9qsFobWIc2YVwe/WPjcnyCkPKtnHAqUYeebc8z0= github.com/Azure/azure-sdk-for-go/sdk/azidentity/cache v0.3.2 h1:yz1bePFlP5Vws5+8ez6T3HWXPmwOK7Yvq8QxDBD3SKY= @@ -14,8 +14,8 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= -github.com/Keyfactor/keyfactor-auth-client-go v1.3.0 h1:otC213b6CYzqeN9b3CRlH1Qj1hTFIN5nqPA8gTlHdLg= -github.com/Keyfactor/keyfactor-auth-client-go v1.3.0/go.mod h1:97vCisBNkdCK0l2TuvOSdjlpvQa4+GHsMut1UTyv1jo= +github.com/Keyfactor/keyfactor-auth-client-go v1.5.0 h1:sq7SGkJeTtDspFSuX2oJxTmFiiFfaQ68B4JP7jryl94= +github.com/Keyfactor/keyfactor-auth-client-go v1.5.0/go.mod h1:rFBZPMSHWwWuUwE1kXhLsDaOxjGiHMbXTTEni8Dmufo= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -28,10 +28,29 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= +github.com/gorilla/securecookie v1.1.1/go.mod h1:ra0sb63/xPlUeL+yeDciTfxMRAA+MP+HVt/4epWDjd4= +github.com/gorilla/sessions v1.2.1 h1:DHd3rPN5lE3Ts3D8rKkQ8x/0kqfeNmBAaiSi+o7FsgI= +github.com/gorilla/sessions v1.2.1/go.mod h1:dk2InVEVJ0sfLlnXv9EAgkf6ecYs/i80K/zI+bUmuGM= github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-uuid v1.0.2/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= +github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/terraform-plugin-log v0.10.0 h1:eu2kW6/QBVdN4P3Ju2WiB2W3ObjkAsyfBsL3Wh1fj3g= github.com/hashicorp/terraform-plugin-log v0.10.0/go.mod h1:/9RR5Cv2aAbrqcTSdNmY1NRHP4E3ekrXRGjqORpXyB0= +github.com/jcmturner/aescts/v2 v2.0.0 h1:9YKLH6ey7H4eDBXW8khjYslgyqG2xZikXP0EQFKrle8= +github.com/jcmturner/aescts/v2 v2.0.0/go.mod h1:AiaICIRyfYg35RUkr8yESTqvSy7csK90qZ5xfvvsoNs= +github.com/jcmturner/dnsutils/v2 v2.0.0 h1:lltnkeZGL0wILNvrNiVCR6Ro5PGU/SeBvVO/8c/iPbo= +github.com/jcmturner/dnsutils/v2 v2.0.0/go.mod h1:b0TnjGOvI/n42bZa+hmXL+kFJZsFT7G4t3HTlQ184QM= +github.com/jcmturner/gofork v1.7.6 h1:QH0l3hzAU1tfT3rZCnW5zXl+orbkNMMRGJfdJjHVETg= +github.com/jcmturner/gofork v1.7.6/go.mod h1:1622LH6i/EZqLloHfE7IeZ0uEJwMSUyQ/nDd82IeqRo= +github.com/jcmturner/goidentity/v6 v6.0.1 h1:VKnZd2oEIMorCTsFBnJWbExfNN7yZr3EhJAxwOkZg6o= +github.com/jcmturner/goidentity/v6 v6.0.1/go.mod h1:X1YW3bgtvwAXju7V3LCIMpY0Gbxyjn/mY9zx4tFonSg= +github.com/jcmturner/gokrb5/v8 v8.4.4 h1:x1Sv4HaTpepFkXbt2IkL29DXRf8sOfZXo8eRKh687T8= +github.com/jcmturner/gokrb5/v8 v8.4.4/go.mod h1:1btQEpgT6k+unzCwX1KdWMEwPPkkgBtP+F6aCACiMrs= +github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZY= +github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU= github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= @@ -59,34 +78,72 @@ github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99 github.com/spbsoluble/go-pkcs12 v0.3.3 h1:3nh7IKn16RDpmrSMtOu1JvbB0XHYq1j+IsICdU1c7J4= github.com/spbsoluble/go-pkcs12 v0.3.3/go.mod h1:MAxKIUEIl/QVcua/I1L4Otyxl9UvLCCIktce2Tjz6Nw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= go.mozilla.org/pkcs7 v0.9.0 h1:yM4/HS9dYv7ri2biPtxt8ikvB37a980dg69/pKmS+eI= go.mozilla.org/pkcs7 v0.9.0/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= -golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q= -golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4= -golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= -golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= -golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo= -golang.org/x/oauth2 v0.33.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= +golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= +golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= +golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= +golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw= +golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= -golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= -golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= +golang.org/x/sys v0.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= +golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= +golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 591d6c27724d71f946f05709928b337b417dec86 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 31 Mar 2026 20:03:32 -0700 Subject: [PATCH 32/51] chore(deps): Bump `github.com/spbsoluble/go-pkcs12 v0.4.0` --- v3/go.mod | 2 +- v3/go.sum | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/v3/go.mod b/v3/go.mod index d64106f..f4eb047 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -21,7 +21,7 @@ toolchain go1.24.5 require ( github.com/Keyfactor/keyfactor-auth-client-go v1.5.0 github.com/hashicorp/terraform-plugin-log v0.10.0 - github.com/spbsoluble/go-pkcs12 v0.3.3 + github.com/spbsoluble/go-pkcs12 v0.4.0 github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 go.mozilla.org/pkcs7 v0.9.0 ) diff --git a/v3/go.sum b/v3/go.sum index e330923..07d9edf 100644 --- a/v3/go.sum +++ b/v3/go.sum @@ -77,6 +77,7 @@ github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/spbsoluble/go-pkcs12 v0.3.3 h1:3nh7IKn16RDpmrSMtOu1JvbB0XHYq1j+IsICdU1c7J4= github.com/spbsoluble/go-pkcs12 v0.3.3/go.mod h1:MAxKIUEIl/QVcua/I1L4Otyxl9UvLCCIktce2Tjz6Nw= +github.com/spbsoluble/go-pkcs12 v0.4.0/go.mod h1:MAxKIUEIl/QVcua/I1L4Otyxl9UvLCCIktce2Tjz6Nw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= From 2e8f2e86f7912936c5b810cbe9890b4a70efb781 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Mon, 20 Apr 2026 13:45:43 -0700 Subject: [PATCH 33/51] fix(certificates): Remove POST field validation, let Command return the error. --- v3/api/certificate.go | 32 +++++--------------------------- 1 file changed, 5 insertions(+), 27 deletions(-) diff --git a/v3/api/certificate.go b/v3/api/certificate.go index 8e281c2..700083e 100644 --- a/v3/api/certificate.go +++ b/v3/api/certificate.go @@ -36,27 +36,6 @@ import ( func (c *Client) EnrollPFX(ea *EnrollPFXFctArgs) (*EnrollResponse, error) { log.Println("[INFO] Enrolling PFX certificate with Keyfactor") - /* Ensure required inputs exist */ - var missingFields []string - - // TODO: Probably a better way to express these if blocks - if ea.Template == "" { - missingFields = append(missingFields, "Template") - } - if ea.CertificateAuthority == "" { - missingFields = append(missingFields, "CertificateAuthority") - } - if ea.CertFormat == "" { - missingFields = append(missingFields, "CertFormat") - } - //if ea.Password == "" { - // missingFields = append(missingFields, "Password") - //} - - if len(missingFields) > 0 { - return nil, errors.New("Required field(s) missing: " + strings.Join(missingFields, ", ")) - } - // Set Keyfactor-specific headers headers := &apiHeaders{ Headers: []StringTuple{ @@ -116,9 +95,6 @@ func (c *Client) EnrollPFXV2(ea *EnrollPFXFctArgsV2) (*EnrollResponseV2, error) if ea.Template == "" && ea.EnrollmentPatternId == 0 { missingFields = append(missingFields, "Template or EnrollmentPatternId") } - if ea.CertificateAuthority == "" { - missingFields = append(missingFields, "CertificateAuthority") - } if ea.CertFormat == "" { missingFields = append(missingFields, "CertFormat") } @@ -344,13 +320,15 @@ func findLeafCert(certs []*x509.Certificate) *x509.Certificate { // enrollment. Required fields to complete a CSR enrollment are: // - CSR : string // - Template : string (or EnrollmentPatternId on Command v25+) -// - CertificateAuthority : string +// - CertificateAuthority : string (optional when using a template or enrollment pattern) func (c *Client) EnrollCSR(ea *EnrollCSRFctArgs) (*EnrollResponse, error) { log.Println("[INFO] Signing CSR with Keyfactor") /* Ensure required inputs exist. - On Command v25+ an EnrollmentPatternId can substitute for Template. */ - if (ea.Template == "" && ea.EnrollmentPatternId == 0) || (ea.CertificateAuthority == "") { + On Command v25+ an EnrollmentPatternId can substitute for Template. + CertificateAuthority is optional when a template or enrollment pattern is provided; + it is only required when enrolling against a standalone CA. */ + if ea.Template == "" && ea.EnrollmentPatternId == 0 { return nil, errors.New("invalid or nonexistent values required for csr enrollment") } From 67363446ced4779de0680b33efa5e9fea7552535 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Mon, 20 Apr 2026 13:45:43 -0700 Subject: [PATCH 34/51] fix(certificates): Remove POST field validation, let Command return the error. --- v3/api/certificate_test.go | 248 +++++++++++++++++++++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 v3/api/certificate_test.go diff --git a/v3/api/certificate_test.go b/v3/api/certificate_test.go new file mode 100644 index 0000000..2e3a642 --- /dev/null +++ b/v3/api/certificate_test.go @@ -0,0 +1,248 @@ +package api + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/base64" + "encoding/json" + "math/big" + "net/http" + "net/http/httptest" + "testing" + "time" + + "go.mozilla.org/pkcs7" +) + +// makeTestCA generates a minimal self-signed CA certificate for testing. +func makeTestCA(t *testing.T) (*rsa.PrivateKey, *x509.Certificate) { + t.Helper() + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatalf("generate CA key: %v", err) + } + tmpl := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "Test Root CA"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(10 * 365 * 24 * time.Hour), + IsCA: true, + BasicConstraintsValid: true, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + } + der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key) + if err != nil { + t.Fatalf("create CA cert: %v", err) + } + cert, err := x509.ParseCertificate(der) + if err != nil { + t.Fatalf("parse CA cert: %v", err) + } + return key, cert +} + +// makeTestLeaf generates a minimal end-entity certificate signed by the given CA. +func makeTestLeaf(t *testing.T, caKey *rsa.PrivateKey, caCert *x509.Certificate) *x509.Certificate { + t.Helper() + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatalf("generate leaf key: %v", err) + } + tmpl := &x509.Certificate{ + SerialNumber: big.NewInt(2), + Subject: pkix.Name{CommonName: "test-leaf.example.com"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(365 * 24 * time.Hour), + IsCA: false, + BasicConstraintsValid: true, + KeyUsage: x509.KeyUsageDigitalSignature, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + } + der, err := x509.CreateCertificate(rand.Reader, tmpl, caCert, &key.PublicKey, caKey) + if err != nil { + t.Fatalf("create leaf cert: %v", err) + } + cert, err := x509.ParseCertificate(der) + if err != nil { + t.Fatalf("parse leaf cert: %v", err) + } + return cert +} + +// buildP7B constructs a degenerate PKCS7 SignedData (P7B) containing the given +// certificates in the provided order, and returns the base64-encoded result. +func buildP7B(t *testing.T, certs ...*x509.Certificate) string { + t.Helper() + sd, err := pkcs7.NewSignedData([]byte{}) + if err != nil { + t.Fatalf("NewSignedData: %v", err) + } + for _, c := range certs { + sd.AddCertificate(c) + } + der, err := sd.Finish() + if err != nil { + t.Fatalf("Finish: %v", err) + } + return base64.StdEncoding.EncodeToString(der) +} + +// TestConvertBase64P7BtoCertificates_RootFirstOrder demonstrates the bug: +// when the P7B contains the root CA as the first certificate (as DigiCert chains +// appear to be returned by Keyfactor), ConvertBase64P7BtoCertificates returns the +// certs in that same order, so certs[0] is the ROOT — not the leaf. +// +// The existing DownloadCertificate code blindly returns certs[0] as the leaf, +// which is wrong. This test reproduces the customer-reported issue where +// common_name, thumbprint etc. are populated with root CA data after a refresh. +func TestConvertBase64P7BtoCertificates_RootFirstOrder(t *testing.T) { + caKey, caCert := makeTestCA(t) + leafCert := makeTestLeaf(t, caKey, caCert) + + // Build P7B with root FIRST, leaf SECOND — mirrors what DigiCert chains look + // like after Keyfactor's ChainOrder:"EndEntityFirst" is ignored by the P7B parser. + p7bBase64 := buildP7B(t, caCert, leafCert) + + certs, err := ConvertBase64P7BtoCertificates(p7bBase64) + if err != nil { + t.Fatalf("ConvertBase64P7BtoCertificates: %v", err) + } + if len(certs) != 2 { + t.Fatalf("expected 2 certs, got %d", len(certs)) + } + + t.Logf("certs[0]: CN=%q IsCA=%v", certs[0].Subject.CommonName, certs[0].IsCA) + t.Logf("certs[1]: CN=%q IsCA=%v", certs[1].Subject.CommonName, certs[1].IsCA) + + // Document the bug: certs[0] is the root CA, not the leaf. + // The current DownloadCertificate code returns certs[0] as "leaf", which is wrong. + if certs[0].Subject.CommonName == "Test Root CA" { + t.Logf("BUG REPRODUCED: certs[0] is the root CA (%q), not the leaf", certs[0].Subject.CommonName) + } + if certs[0].Subject.CommonName == leafCert.Subject.CommonName { + t.Logf("Order preserved leaf-first — P7B ordering is end-entity-first in this run") + } + + // The leaf cert must exist somewhere in the list. + leafFound := false + for _, c := range certs { + if c.Subject.CommonName == "test-leaf.example.com" { + leafFound = true + } + } + if !leafFound { + t.Error("leaf cert not found in parsed P7B at all") + } +} + +// TestConvertBase64P7BtoCertificates_LeafFirstOrder verifies that when the P7B +// has the leaf first, the current code works correctly (certs[0] = leaf). +// This shows the code is position-dependent, not validated. +func TestConvertBase64P7BtoCertificates_LeafFirstOrder(t *testing.T) { + caKey, caCert := makeTestCA(t) + leafCert := makeTestLeaf(t, caKey, caCert) + + // Build P7B with leaf FIRST, root SECOND. + p7bBase64 := buildP7B(t, leafCert, caCert) + + certs, err := ConvertBase64P7BtoCertificates(p7bBase64) + if err != nil { + t.Fatalf("ConvertBase64P7BtoCertificates: %v", err) + } + if len(certs) != 2 { + t.Fatalf("expected 2 certs, got %d", len(certs)) + } + + t.Logf("certs[0]: CN=%q IsCA=%v", certs[0].Subject.CommonName, certs[0].IsCA) + t.Logf("certs[1]: CN=%q IsCA=%v", certs[1].Subject.CommonName, certs[1].IsCA) + + // When the leaf is first, the current code happens to return the right cert. + // But this is accidental — it depends on P7B cert ordering. + if certs[0].Subject.CommonName == leafCert.Subject.CommonName { + t.Logf("leaf-first P7B: certs[0] is correctly the leaf (by luck of ordering)") + } +} + +// mockDownloadServer returns a TLS test server that responds to any POST with a +// JSON body {"Content": p7bBase64}. +func mockDownloadServer(t *testing.T, p7bBase64 string) *httptest.Server { + t.Helper() + body, err := json.Marshal(map[string]string{"Content": p7bBase64}) + if err != nil { + t.Fatalf("marshal mock response: %v", err) + } + return httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write(body) + })) +} + +// TestDownloadCertificate_RootFirstP7B is the primary regression test for the +// cert-chain ordering bug. When the server returns a P7B with the root CA first +// (as DigiCert chains do), DownloadCertificate must still return the end-entity +// cert as the leaf — not the root CA. +// +// This test FAILS against the buggy code and PASSES after the fix. +func TestDownloadCertificate_RootFirstP7B(t *testing.T) { + caKey, caCert := makeTestCA(t) + leafCert := makeTestLeaf(t, caKey, caCert) + + // Root-first ordering — the problematic case reported by customers using DigiCert. + server := mockDownloadServer(t, buildP7B(t, caCert, leafCert)) + defer server.Close() + + client := newTestClient(server) + leaf, chain, _, err := client.DownloadCertificate(1, "", "", "", 0, "P7B") + if err != nil { + t.Fatalf("DownloadCertificate: %v", err) + } + if leaf == nil { + t.Fatal("leaf is nil") + } + if chain == nil || len(chain) != 2 { + t.Fatalf("expected chain of length 2, got %v", chain) + } + + // Core assertion: the returned leaf must be the end-entity, not the CA. + if leaf.IsCA { + t.Errorf("DownloadCertificate returned a CA cert as leaf: CN=%q IsCA=%v; want end-entity CN=%q", + leaf.Subject.CommonName, leaf.IsCA, leafCert.Subject.CommonName) + } + if leaf.Subject.CommonName != leafCert.Subject.CommonName { + t.Errorf("leaf CN = %q, want %q", leaf.Subject.CommonName, leafCert.Subject.CommonName) + } +} + +// TestDownloadCertificate_LeafFirstP7B verifies the common case where the server +// returns a leaf-first P7B (most internal CAs). The fix must not break this. +func TestDownloadCertificate_LeafFirstP7B(t *testing.T) { + caKey, caCert := makeTestCA(t) + leafCert := makeTestLeaf(t, caKey, caCert) + + // Leaf-first ordering — the "lucky" path that has always worked. + server := mockDownloadServer(t, buildP7B(t, leafCert, caCert)) + defer server.Close() + + client := newTestClient(server) + leaf, chain, _, err := client.DownloadCertificate(1, "", "", "", 0, "P7B") + if err != nil { + t.Fatalf("DownloadCertificate: %v", err) + } + if leaf == nil { + t.Fatal("leaf is nil") + } + if chain == nil || len(chain) != 2 { + t.Fatalf("expected chain of length 2, got %v", chain) + } + + if leaf.IsCA { + t.Errorf("DownloadCertificate returned a CA cert as leaf: CN=%q IsCA=%v; want end-entity CN=%q", + leaf.Subject.CommonName, leaf.IsCA, leafCert.Subject.CommonName) + } + if leaf.Subject.CommonName != leafCert.Subject.CommonName { + t.Errorf("leaf CN = %q, want %q", leaf.Subject.CommonName, leafCert.Subject.CommonName) + } +} From 5a3f181d9bd478c99617ef44e268eb019b2b24d2 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 28 Apr 2026 12:38:30 -0700 Subject: [PATCH 35/51] fix: paginate ListApplications to return all results (not just first 50) The Applications API returns at most 50 results per page. With 50+ apps in the lab, newly created apps were not visible to ListApplications, causing TestIntKeyfactorApplicationDataSource to fail consistently. Pagination uses existing PageReturned/ReturnLimit query params (same pattern as CertificateStoreTypes). Regression tests added. --- v3/api/application.go | 44 ++++++++++++-------- v3/api/application_test.go | 82 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 110 insertions(+), 16 deletions(-) create mode 100644 v3/api/application_test.go diff --git a/v3/api/application.go b/v3/api/application.go index d8002ad..132e439 100644 --- a/v3/api/application.go +++ b/v3/api/application.go @@ -55,7 +55,7 @@ func (c *Client) appEndpoint() string { return "Applications" } -// ListApplications returns all applications/containers. +// ListApplications returns all applications/containers, paginating automatically. func (c *Client) ListApplications() ([]ApplicationListItem, error) { log.Println("[INFO] Listing applications.") @@ -66,22 +66,34 @@ func (c *Client) ListApplications() ([]ApplicationListItem, error) { }, } - req := &request{ - Method: "GET", - Endpoint: c.appEndpoint(), - Headers: headers, - } - - resp, err := c.sendRequest(req) - if err != nil { - return nil, err - } - - var result []ApplicationListItem - if err = json.NewDecoder(resp.Body).Decode(&result); err != nil { - return nil, err + const pageSize = 100 + var all []ApplicationListItem + for page := 1; ; page++ { + req := &request{ + Method: "GET", + Endpoint: c.appEndpoint(), + Headers: headers, + Query: &apiQuery{ + Query: []StringTuple{ + {"PageReturned", strconv.Itoa(page)}, + {"ReturnLimit", strconv.Itoa(pageSize)}, + }, + }, + } + resp, err := c.sendRequest(req) + if err != nil { + return nil, err + } + var pageResults []ApplicationListItem + if err = json.NewDecoder(resp.Body).Decode(&pageResults); err != nil { + return nil, err + } + all = append(all, pageResults...) + if len(pageResults) < pageSize { + break + } } - return result, nil + return all, nil } // GetApplication returns the full details of an application/container by integer ID. diff --git a/v3/api/application_test.go b/v3/api/application_test.go new file mode 100644 index 0000000..8302061 --- /dev/null +++ b/v3/api/application_test.go @@ -0,0 +1,82 @@ +package api + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strconv" + "testing" +) + +// TestListApplications_Pagination verifies that ListApplications fetches all pages. +// Before the fix, only the first 50 results were returned; a newly created app on +// page 2+ would never be found by name, causing a consistent integration test failure. +func TestListApplications_Pagination(t *testing.T) { + const totalApps = 150 + + allApps := make([]ApplicationListItem, totalApps) + for i := range allApps { + allApps[i] = ApplicationListItem{Id: i + 1, Name: "app-" + strconv.Itoa(i+1)} + } + + srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + page, _ := strconv.Atoi(r.URL.Query().Get("PageReturned")) + limit, _ := strconv.Atoi(r.URL.Query().Get("ReturnLimit")) + if page < 1 { + page = 1 + } + if limit < 1 { + limit = 50 + } + start := (page - 1) * limit + end := start + limit + if start >= len(allApps) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("[]")) + return + } + if end > len(allApps) { + end = len(allApps) + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(allApps[start:end]) + })) + defer srv.Close() + + c := newTestClient(srv) + + apps, err := c.ListApplications() + if err != nil { + t.Fatalf("ListApplications() error: %v", err) + } + if len(apps) != totalApps { + t.Errorf("ListApplications() returned %d apps, want %d (pagination broken)", len(apps), totalApps) + } + if apps[len(apps)-1].Id != totalApps { + t.Errorf("last app ID = %d, want %d (page 2 results missing)", apps[len(apps)-1].Id, totalApps) + } +} + +// TestListApplications_SinglePage verifies a sub-pageSize result terminates the loop. +func TestListApplications_SinglePage(t *testing.T) { + calls := 0 + srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode([]ApplicationListItem{{Id: 1, Name: "only-app"}}) + })) + defer srv.Close() + + c := newTestClient(srv) + + apps, err := c.ListApplications() + if err != nil { + t.Fatalf("ListApplications() error: %v", err) + } + if len(apps) != 1 { + t.Errorf("got %d apps, want 1", len(apps)) + } + if calls != 1 { + t.Errorf("server called %d times, want 1", calls) + } +} From 856af8046ca0a2c719d21da4aa846c9cd681d1a2 Mon Sep 17 00:00:00 2001 From: spb <1661003+spbsoluble@users.noreply.github.com> Date: Tue, 16 Jun 2026 12:14:30 -0700 Subject: [PATCH 36/51] fix(certificate): UnpackPEM selects leaf by topology, not position (#53) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary `UnpackPEM` selected the leaf certificate **positionally** — it assumed `certificates[0]` was the end-entity leaf: ```go // before certificate = certificates[0] caCertificates = certificates[1:] ``` This returns the **root CA** as the leaf whenever Keyfactor Command sends a PEM bundle that is not leaf-first. Externally-rooted chains (e.g. DigiCert PKIaaS) are commonly returned **root-first**, so `certificates[0]` is the root. Consumers that trust the returned leaf (e.g. the Terraform provider populating `common_name` / `certificate_pem`) then persist the root CA's subject, forcing certificate replacement on every run. ## Fix Select the leaf by chain topology using the package's existing `findLeafCert` (the cert no other cert in the set issued) — the same helper `DownloadCertificate` already uses for the P7B path. The remaining certs become the CA chain, preserving their original order. Falls back to index 0 when no certs parse, preserving prior behavior for degenerate inputs. This makes leaf selection order-independent and consistent across the P7B and PEM code paths. ## Also included - **go.sum:** added the missing `github.com/spbsoluble/go-pkcs12 v0.4.0` module zip checksum (`h1:`). `go.mod` pins v0.4.0 but `go.sum` only carried the `/go.mod` hash, so clean builds failed with `missing go.sum entry for module providing package github.com/spbsoluble/go-pkcs12`. ## Tests New `v3/api/unpackpem_leaf_test.go`: - `TestUnpackPEM_LeafSelection` — root-first / leaf-first / shuffled orderings, 2- and 3-cert chains; asserts the non-CA leaf is selected and the chain length is correct. - `TestUnpackPEM_WithPrivateKey_RootFirst` — root-first bundle with a private key block; asserts both key extraction and correct leaf selection. - `TestUnpackPEM_SingleCert` — single cert returned as leaf, empty chain. Verified **red→green**: the root-first / shuffled cases fail against the pre-fix code (return `Test Root CA`) and pass after the fix. Full `./api/...` suite green. Fixes #52 --- v3/api/helpers.go | 41 ++++++++- v3/api/unpackpem_leaf_test.go | 165 ++++++++++++++++++++++++++++++++++ v3/go.sum | 1 + 3 files changed, 203 insertions(+), 4 deletions(-) create mode 100644 v3/api/unpackpem_leaf_test.go diff --git a/v3/api/helpers.go b/v3/api/helpers.go index c57f590..ad6b571 100644 --- a/v3/api/helpers.go +++ b/v3/api/helpers.go @@ -64,6 +64,7 @@ func UnpackPEM(pemData interface{}, password string) ( } var certificates []string + var parsedCerts []*x509.Certificate var encryptedKeyBlock *pem.Block // Parse all PEM blocks @@ -79,6 +80,13 @@ func UnpackPEM(pemData interface{}, password string) ( case "CERTIFICATE": certPEM := string(pem.EncodeToMemory(block)) certificates = append(certificates, certPEM) + // Keep a parsed copy (index-aligned with certificates) so the leaf + // can be selected by chain topology rather than by position. + if c, parseErr := x509.ParseCertificate(block.Bytes); parseErr == nil && c != nil { + parsedCerts = append(parsedCerts, c) + } else { + parsedCerts = append(parsedCerts, nil) + } case "ENCRYPTED PRIVATE KEY": encryptedKeyBlock = block case "RSA PRIVATE KEY", "EC PRIVATE KEY", "PRIVATE KEY": @@ -97,11 +105,36 @@ func UnpackPEM(pemData interface{}, password string) ( privateKey = decryptedKey } - // Assign certificates: first is leaf, rest are CA chain + // Select the leaf by chain topology, not position: Keyfactor Command may + // return the bundle in any order (notably root-first for externally-rooted + // chains such as DigiCert PKIaaS), so certificates[0] is not reliably the + // end-entity. findLeafCert picks the cert no other cert in the set issued, + // matching the behavior of DownloadCertificate. The remaining certs become + // the CA chain, preserving their original order. if len(certificates) > 0 { - certificate = certificates[0] - if len(certificates) > 1 { - caCertificates = certificates[1:] + leafIdx := 0 + // Collect the successfully-parsed certs for leaf detection. + var valid []*x509.Certificate + for _, c := range parsedCerts { + if c != nil { + valid = append(valid, c) + } + } + if leaf := findLeafCert(valid); leaf != nil { + for i, c := range parsedCerts { + if c != nil && c.Equal(leaf) { + leafIdx = i + break + } + } + } + + certificate = certificates[leafIdx] + for i, certPEM := range certificates { + if i == leafIdx { + continue + } + caCertificates = append(caCertificates, certPEM) } } diff --git a/v3/api/unpackpem_leaf_test.go b/v3/api/unpackpem_leaf_test.go new file mode 100644 index 0000000..0a19c23 --- /dev/null +++ b/v3/api/unpackpem_leaf_test.go @@ -0,0 +1,165 @@ +package api + +// Regression tests for UnpackPEM leaf selection. UnpackPEM previously assumed +// certificates[0] was the end-entity leaf, which returns the ROOT when Keyfactor +// Command sends a non-leaf-first PEM bundle (e.g. externally-rooted chains +// returned root-first). It now selects the leaf by chain topology via +// findLeafCert, matching DownloadCertificate. + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "crypto/x509/pkix" + "encoding/pem" + "math/big" + "testing" + "time" +) + +func pemCert(c *x509.Certificate) string { + return string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: c.Raw})) +} + +// makeTestIntermediateCA creates an intermediate CA signed by the given parent. +func makeTestIntermediateCA(t *testing.T, parentKey *rsa.PrivateKey, parentCert *x509.Certificate) (*rsa.PrivateKey, *x509.Certificate) { + t.Helper() + key, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatalf("generate intermediate key: %v", err) + } + tmpl := &x509.Certificate{ + SerialNumber: big.NewInt(10), + Subject: pkix.Name{CommonName: "Test Intermediate CA"}, + NotBefore: time.Now().Add(-time.Hour), + NotAfter: time.Now().Add(5 * 365 * 24 * time.Hour), + IsCA: true, + BasicConstraintsValid: true, + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + } + der, err := x509.CreateCertificate(rand.Reader, tmpl, parentCert, &key.PublicKey, parentKey) + if err != nil { + t.Fatalf("create intermediate cert: %v", err) + } + cert, err := x509.ParseCertificate(der) + if err != nil { + t.Fatalf("parse intermediate cert: %v", err) + } + return key, cert +} + +// assertUnpackLeaf runs UnpackPEM and asserts the selected leaf is the expected +// end-entity (non-CA) cert and the CA chain has the expected length. +func assertUnpackLeaf(t *testing.T, bundle, wantCN string, wantChainLen int) { + t.Helper() + _, certificate, caCerts, err := UnpackPEM(bundle, "") + if err != nil { + t.Fatalf("UnpackPEM: %v", err) + } + block, _ := pem.Decode([]byte(certificate)) + if block == nil { + t.Fatalf("returned leaf is not a PEM block: %q", certificate) + } + c, err := x509.ParseCertificate(block.Bytes) + if err != nil { + t.Fatalf("parse returned leaf: %v", err) + } + if c.IsCA { + t.Errorf("UnpackPEM returned a CA as the leaf: CN=%q IsCA=true", c.Subject.CommonName) + } + if c.Subject.CommonName != wantCN { + t.Errorf("leaf CN = %q, want %q", c.Subject.CommonName, wantCN) + } + if len(caCerts) != wantChainLen { + t.Errorf("caCertificates length = %d, want %d", len(caCerts), wantChainLen) + } +} + +// TestUnpackPEM_LeafSelection verifies the end-entity leaf is selected for every +// bundle ordering, including the root-first orderings that fooled the old +// positional certificates[0] logic. +func TestUnpackPEM_LeafSelection(t *testing.T) { + rootKey, root := makeTestCA(t) + intKey, intermediate := makeTestIntermediateCA(t, rootKey, root) + leaf := makeTestLeaf(t, intKey, intermediate) + + rootPEM := pemCert(root) + intPEM := pemCert(intermediate) + leafPEM := pemCert(leaf) + + cases := []struct { + name string + bundle string + chainLen int + }{ + {"root-first-3cert", rootPEM + intPEM + leafPEM, 2}, // the bug trigger + {"leaf-first-3cert", leafPEM + intPEM + rootPEM, 2}, + {"shuffled-3cert", rootPEM + leafPEM + intPEM, 2}, + {"root-first-2cert", rootPEM + leafPEM, 1}, // the bug trigger + {"leaf-first-2cert", leafPEM + rootPEM, 1}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + assertUnpackLeaf(t, tc.bundle, "test-leaf.example.com", tc.chainLen) + }) + } +} + +// TestUnpackPEM_WithPrivateKey_RootFirst verifies that, with a private key block +// present in a root-first bundle, both the key is extracted and the leaf (not the +// root) is selected. +func TestUnpackPEM_WithPrivateKey_RootFirst(t *testing.T) { + rootKey, root := makeTestCA(t) + leaf := makeTestLeaf(t, rootKey, root) + + k, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatalf("generate key: %v", err) + } + keyDER, err := x509.MarshalPKCS8PrivateKey(k) + if err != nil { + t.Fatalf("marshal key: %v", err) + } + keyPEM := string(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: keyDER})) + + // key + ROOT-first certs + bundle := keyPEM + pemCert(root) + pemCert(leaf) + + privateKey, certificate, caCerts, err := UnpackPEM(bundle, "") + if err != nil { + t.Fatalf("UnpackPEM: %v", err) + } + if privateKey == "" { + t.Error("expected private key to be extracted") + } + block, _ := pem.Decode([]byte(certificate)) + if block == nil { + t.Fatalf("returned leaf is not a PEM block") + } + c, err := x509.ParseCertificate(block.Bytes) + if err != nil { + t.Fatalf("parse returned leaf: %v", err) + } + if c.IsCA || c.Subject.CommonName != "test-leaf.example.com" { + t.Errorf("leaf = CN=%q IsCA=%v, want test-leaf.example.com (non-CA)", c.Subject.CommonName, c.IsCA) + } + if len(caCerts) != 1 { + t.Errorf("caCertificates length = %d, want 1", len(caCerts)) + } +} + +// TestUnpackPEM_SingleCert returns the only cert as the leaf, no chain. +func TestUnpackPEM_SingleCert(t *testing.T) { + rootKey, root := makeTestCA(t) + leaf := makeTestLeaf(t, rootKey, root) + _, certificate, caCerts, err := UnpackPEM(pemCert(leaf), "") + if err != nil { + t.Fatalf("UnpackPEM: %v", err) + } + if certificate == "" { + t.Fatal("expected a certificate") + } + if len(caCerts) != 0 { + t.Errorf("caCertificates length = %d, want 0", len(caCerts)) + } +} diff --git a/v3/go.sum b/v3/go.sum index 07d9edf..451aad8 100644 --- a/v3/go.sum +++ b/v3/go.sum @@ -77,6 +77,7 @@ github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/spbsoluble/go-pkcs12 v0.3.3 h1:3nh7IKn16RDpmrSMtOu1JvbB0XHYq1j+IsICdU1c7J4= github.com/spbsoluble/go-pkcs12 v0.3.3/go.mod h1:MAxKIUEIl/QVcua/I1L4Otyxl9UvLCCIktce2Tjz6Nw= +github.com/spbsoluble/go-pkcs12 v0.4.0 h1:3HOVPZ8pvYqhAyz/NJzT9YODQJ3HbZvB9/CMVmvGaUM= github.com/spbsoluble/go-pkcs12 v0.4.0/go.mod h1:MAxKIUEIl/QVcua/I1L4Otyxl9UvLCCIktce2Tjz6Nw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= From 3f35bf80da3bfda61333eed287fbcc83a6f02afd Mon Sep 17 00:00:00 2001 From: spb <1661003+spbsoluble@users.noreply.github.com> Date: Fri, 17 Jul 2026 12:47:43 -0700 Subject: [PATCH 37/51] fix(template): paginate GetTemplates to return all templates (#55) Closes #54 --- v3/api/template.go | 68 +++++++++++++++------ v3/api/template_test.go | 132 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 183 insertions(+), 17 deletions(-) create mode 100644 v3/api/template_test.go diff --git a/v3/api/template.go b/v3/api/template.go index 73e7423..161f703 100644 --- a/v3/api/template.go +++ b/v3/api/template.go @@ -18,8 +18,15 @@ import ( "encoding/json" "errors" "fmt" + "log" + "strconv" ) +// getTemplatesMaxPages is the maximum number of pages GetTemplates will fetch +// before aborting with an error. It is an unexported package-level var (not a +// const) so that tests can lower it without iterating thousands of times. +var getTemplatesMaxPages = 10000 + // GetTemplate takes arguments for a template ID used to facilitate the retrieval // of certificate template context. The primary query required to get certificate context is the template ID. A pointer // to a GetTemplateResponse structure is returned, containing the template context. @@ -59,9 +66,12 @@ func (c *Client) GetTemplate(Id interface{}) (*GetTemplateResponse, error) { return jsonResp, err } -// GetTemplates asks Keyfactor for a complete list of known certificate templates. A list of -// GetTemplateResponse structures is returned, containing the template context. +// GetTemplates asks Keyfactor for a complete list of known certificate templates, +// paginating automatically so that instances with more than the server's default +// page size (50) return all templates. A list of GetTemplateResponse structures +// is returned, containing the template context. func (c *Client) GetTemplates() ([]GetTemplateResponse, error) { + log.Println("[INFO] Listing certificate templates.") // Set Keyfactor-specific headers headers := &apiHeaders{ @@ -71,25 +81,49 @@ func (c *Client) GetTemplates() ([]GetTemplateResponse, error) { }, } - keyfactorAPIStruct := &request{ - Method: "GET", - Endpoint: "Templates/", - Headers: headers, - Query: nil, - Payload: nil, + const pageSize = 100 + var all []GetTemplateResponse + var page int + for page = 1; page <= getTemplatesMaxPages; page++ { + keyfactorAPIStruct := &request{ + Method: "GET", + Endpoint: "Templates/", + Headers: headers, + Query: &apiQuery{ + Query: []StringTuple{ + {"PageReturned", strconv.Itoa(page)}, + {"ReturnLimit", strconv.Itoa(pageSize)}, + }, + }, + Payload: nil, + } + + resp, err := c.sendRequest(keyfactorAPIStruct) + if err != nil { + log.Printf("[ERROR] GetTemplates: request for page %d failed: %s", page, err) + return nil, err + } + + var pageResults []GetTemplateResponse + decodeErr := json.NewDecoder(resp.Body).Decode(&pageResults) + resp.Body.Close() + if decodeErr != nil { + log.Printf("[ERROR] GetTemplates: failed to decode page %d: %s", page, decodeErr) + return nil, decodeErr + } + + all = append(all, pageResults...) + if len(pageResults) == 0 || len(pageResults) < pageSize { + break + } } - resp, err := c.sendRequest(keyfactorAPIStruct) - if err != nil { - return nil, err + if page > getTemplatesMaxPages { + return nil, fmt.Errorf("GetTemplates: exceeded max pages (%d); server may be ignoring pagination", getTemplatesMaxPages) } - var jsonResp []GetTemplateResponse - err = json.NewDecoder(resp.Body).Decode(&jsonResp) - if err != nil { - return nil, err - } - return jsonResp, err + log.Printf("[INFO] Listed %d certificate templates across %d page(s).", len(all), page) + return all, nil } // UpdateTemplate takes arguments for a UpdateTemplateArg structure used to facilitate the modification diff --git a/v3/api/template_test.go b/v3/api/template_test.go new file mode 100644 index 0000000..d6bc71a --- /dev/null +++ b/v3/api/template_test.go @@ -0,0 +1,132 @@ +package api + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strconv" + "testing" +) + +// TestGetTemplates_Pagination verifies that GetTemplates fetches all pages. +// Before the fix, only the first 50 results were returned; a template sorted +// beyond position 50 (e.g. position 169 out of 278) would never be found by +// name, causing "Error template name not found" in keyfactor_template_role_binding. +func TestGetTemplates_Pagination(t *testing.T) { + const totalTemplates = 278 + + allTemplates := make([]GetTemplateResponse, totalTemplates) + for i := range allTemplates { + allTemplates[i] = GetTemplateResponse{ + Id: i + 1, + CommonName: "Template-" + strconv.Itoa(i+1), + } + } + // Place the known-problematic late-sorted template at index 168 (position 169). + allTemplates[168] = GetTemplateResponse{Id: 243, CommonName: "zzz-late-sorted-template"} + + srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + page, _ := strconv.Atoi(r.URL.Query().Get("PageReturned")) + limit, _ := strconv.Atoi(r.URL.Query().Get("ReturnLimit")) + if page < 1 { + page = 1 + } + if limit < 1 { + limit = 50 + } + start := (page - 1) * limit + end := start + limit + if start >= len(allTemplates) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("[]")) + return + } + if end > len(allTemplates) { + end = len(allTemplates) + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(allTemplates[start:end]) + })) + defer srv.Close() + + c := newTestClient(srv) + + templates, err := c.GetTemplates() + if err != nil { + t.Fatalf("GetTemplates() error: %v", err) + } + if len(templates) != totalTemplates { + t.Errorf("GetTemplates() returned %d templates, want %d (pagination broken)", len(templates), totalTemplates) + } + + // Verify the late-sorted target template is present. + found := false + for _, tmpl := range templates { + if tmpl.CommonName == "zzz-late-sorted-template" { + found = true + if tmpl.Id != 243 { + t.Errorf("target template ID = %d, want 243", tmpl.Id) + } + break + } + } + if !found { + t.Errorf("target template %q (position 169) not found — page 2+ results missing", "zzz-late-sorted-template") + } +} + +// TestGetTemplates_MaxPagesGuard verifies that GetTemplates aborts with an error +// when the server always returns a full page (simulating a server that ignores +// pagination and would otherwise cause an infinite loop / unbounded memory growth). +func TestGetTemplates_MaxPagesGuard(t *testing.T) { + // Build a fixed full page of pageSize (100) items. + fullPage := make([]GetTemplateResponse, 100) + for i := range fullPage { + fullPage[i] = GetTemplateResponse{Id: i + 1, CommonName: "Template-" + strconv.Itoa(i+1)} + } + + srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Always return a full page regardless of PageReturned — simulates a + // server that ignores paging parameters. + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(fullPage) + })) + defer srv.Close() + + // Lower the safety bound so the test terminates quickly. + orig := getTemplatesMaxPages + getTemplatesMaxPages = 3 + defer func() { getTemplatesMaxPages = orig }() + + c := newTestClient(srv) + + _, err := c.GetTemplates() + if err == nil { + t.Fatal("GetTemplates() expected an error when max pages exceeded, got nil") + } +} + +// TestGetTemplates_SinglePage verifies that a sub-pageSize result terminates +// the pagination loop in a single call. +func TestGetTemplates_SinglePage(t *testing.T) { + calls := 0 + srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode([]GetTemplateResponse{{Id: 1, CommonName: "OnlyTemplate"}}) + })) + defer srv.Close() + + c := newTestClient(srv) + + templates, err := c.GetTemplates() + if err != nil { + t.Fatalf("GetTemplates() error: %v", err) + } + if len(templates) != 1 { + t.Errorf("got %d templates, want 1", len(templates)) + } + if calls != 1 { + t.Errorf("server called %d times, want 1", calls) + } +} From 4141aec03dc2e4c65ba0c406148eff79e73015cc Mon Sep 17 00:00:00 2001 From: spb <1661003+spbsoluble@users.noreply.github.com> Date: Fri, 17 Jul 2026 12:48:41 -0700 Subject: [PATCH 38/51] fix(store): nullable bool flags, EntryPassword omitempty, paginate GetStoreContainers (#56) --- v3/api/store_container.go | 63 ++++++++++---- v3/api/store_container_test.go | 155 +++++++++++++++++++++++++++++++++ v3/api/store_models.go | 4 +- v3/api/store_type_models.go | 8 +- 4 files changed, 210 insertions(+), 20 deletions(-) create mode 100644 v3/api/store_container_test.go diff --git a/v3/api/store_container.go b/v3/api/store_container.go index 74ec154..634f9fa 100644 --- a/v3/api/store_container.go +++ b/v3/api/store_container.go @@ -22,7 +22,15 @@ import ( "strconv" ) -// GetStoreContainers returns a list of store containers +// getStoreContainersMaxPages is the maximum number of pages GetStoreContainers +// will fetch before aborting with an error. It is an unexported package-level +// var (not a const) so that tests can lower it without iterating thousands of +// times. +var getStoreContainersMaxPages = 10000 + +// GetStoreContainers returns a list of store containers, paginating +// automatically so that instances with more than the server's default page +// size (100) return all containers. func (c *Client) GetStoreContainers() (*[]CertStoreContainer, error) { log.Println("[INFO] Listing certificate store containers.") @@ -33,24 +41,49 @@ func (c *Client) GetStoreContainers() (*[]CertStoreContainer, error) { }, } - keyfactorAPIStruct := &request{ - Method: "GET", - Endpoint: "CertificateStoreContainers", - Headers: headers, - Payload: nil, - } + const pageSize = 100 + var all []CertStoreContainer + var page int + for page = 1; page <= getStoreContainersMaxPages; page++ { + keyfactorAPIStruct := &request{ + Method: "GET", + Endpoint: "CertificateStoreContainers", + Headers: headers, + Query: &apiQuery{ + Query: []StringTuple{ + {"PageReturned", strconv.Itoa(page)}, + {"ReturnLimit", strconv.Itoa(pageSize)}, + }, + }, + Payload: nil, + } - resp, err := c.sendRequest(keyfactorAPIStruct) - if err != nil { - return nil, err + resp, err := c.sendRequest(keyfactorAPIStruct) + if err != nil { + log.Printf("[ERROR] GetStoreContainers: request for page %d failed: %s", page, err) + return nil, err + } + + var pageResults []CertStoreContainer + decodeErr := json.NewDecoder(resp.Body).Decode(&pageResults) + resp.Body.Close() + if decodeErr != nil { + log.Printf("[ERROR] GetStoreContainers: failed to decode page %d: %s", page, decodeErr) + return nil, decodeErr + } + + all = append(all, pageResults...) + if len(pageResults) == 0 || len(pageResults) < pageSize { + break + } } - jsonResp := &[]CertStoreContainer{} - err = json.NewDecoder(resp.Body).Decode(&jsonResp) - if err != nil { - return nil, err + if page > getStoreContainersMaxPages { + return nil, fmt.Errorf("GetStoreContainers: exceeded max pages (%d); server may be ignoring pagination", getStoreContainersMaxPages) } - return jsonResp, nil + + log.Printf("[INFO] Listed %d certificate store containers across %d page(s).", len(all), page) + return &all, nil } // GetStoreContainer takes an ID and returns a single store container diff --git a/v3/api/store_container_test.go b/v3/api/store_container_test.go new file mode 100644 index 0000000..2c1daf9 --- /dev/null +++ b/v3/api/store_container_test.go @@ -0,0 +1,155 @@ +// Copyright 2024 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "strconv" + "testing" +) + +// TestGetStoreContainers_Pagination verifies that GetStoreContainers fetches +// all pages. Before the fix, only the server's first page (default page size) +// was returned; a container sorted beyond that first page would never be +// found by ID via the list-endpoint fallback in the provider's +// lookupContainerNameByID, causing the container name to silently read back +// as null even though the container assignment itself was intact. +func TestGetStoreContainers_Pagination(t *testing.T) { + const totalContainers = 150 + + allContainers := make([]CertStoreContainer, totalContainers) + for i := range allContainers { + id := i + 1 + allContainers[i] = CertStoreContainer{ + Id: &id, + Name: "Container-" + strconv.Itoa(id), + } + } + // Place the known-problematic late-sorted container at index 118 (position 119, past a 100-item first page). + lateId := 999 + allContainers[118] = CertStoreContainer{Id: &lateId, Name: "zzz-late-sorted-container"} + + srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + page, _ := strconv.Atoi(r.URL.Query().Get("PageReturned")) + limit, _ := strconv.Atoi(r.URL.Query().Get("ReturnLimit")) + if page < 1 { + page = 1 + } + if limit < 1 { + limit = 50 + } + start := (page - 1) * limit + end := start + limit + if start >= len(allContainers) { + w.Header().Set("Content-Type", "application/json") + w.Write([]byte("[]")) + return + } + if end > len(allContainers) { + end = len(allContainers) + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(allContainers[start:end]) + })) + defer srv.Close() + + c := newTestClient(srv) + + containers, err := c.GetStoreContainers() + if err != nil { + t.Fatalf("GetStoreContainers() error: %v", err) + } + if containers == nil || len(*containers) != totalContainers { + got := 0 + if containers != nil { + got = len(*containers) + } + t.Errorf("GetStoreContainers() returned %d containers, want %d (pagination broken)", got, totalContainers) + } + + found := false + for _, container := range *containers { + if container.Name == "zzz-late-sorted-container" { + found = true + if container.Id == nil || *container.Id != lateId { + t.Errorf("target container Id = %v, want %d", container.Id, lateId) + } + break + } + } + if !found { + t.Errorf("target container %q (position 119) not found — page 2+ results missing", "zzz-late-sorted-container") + } +} + +// TestGetStoreContainers_MaxPagesGuard verifies that GetStoreContainers +// aborts with an error when the server always returns a full page +// (simulating a server that ignores pagination and would otherwise cause an +// infinite loop / unbounded memory growth). +func TestGetStoreContainers_MaxPagesGuard(t *testing.T) { + fullPage := make([]CertStoreContainer, 100) + for i := range fullPage { + id := i + 1 + fullPage[i] = CertStoreContainer{Id: &id, Name: "Container-" + strconv.Itoa(id)} + } + + srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Always return a full page regardless of PageReturned — simulates a + // server that ignores paging parameters. + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(fullPage) + })) + defer srv.Close() + + // Lower the safety bound so the test terminates quickly. + orig := getStoreContainersMaxPages + getStoreContainersMaxPages = 3 + defer func() { getStoreContainersMaxPages = orig }() + + c := newTestClient(srv) + + _, err := c.GetStoreContainers() + if err == nil { + t.Fatal("GetStoreContainers() expected an error when max pages exceeded, got nil") + } +} + +// TestGetStoreContainers_SinglePage verifies that a sub-pageSize result +// terminates the pagination loop in a single call. +func TestGetStoreContainers_SinglePage(t *testing.T) { + calls := 0 + srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + calls++ + id := 1 + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode([]CertStoreContainer{{Id: &id, Name: "OnlyContainer"}}) + })) + defer srv.Close() + + c := newTestClient(srv) + + containers, err := c.GetStoreContainers() + if err != nil { + t.Fatalf("GetStoreContainers() error: %v", err) + } + if containers == nil || len(*containers) != 1 { + t.Errorf("got %v containers, want 1", containers) + } + if calls != 1 { + t.Errorf("server called %d times, want 1", calls) + } +} diff --git a/v3/api/store_models.go b/v3/api/store_models.go index 0486fea..888cc7b 100644 --- a/v3/api/store_models.go +++ b/v3/api/store_models.go @@ -294,7 +294,9 @@ type CertificateStore struct { Overwrite bool `json:"Overwrite,omitempty"` // The password to set on the entry within the certificate store, if applicable. Only select certificate stores support entry passwords (e.g. Java keystores). - EntryPassword *EntryPassword `json:"EntryPassword"` + // omitempty: a nil pointer must be omitted, not marshaled as an explicit + // "EntryPassword": null on every add-certificate-to-store request. + EntryPassword *EntryPassword `json:"EntryPassword,omitempty"` // Password used to secure certificate store, if it exists as a PKCS#12 PfxPassword string `json:"PfxPassword,omitempty"` diff --git a/v3/api/store_type_models.go b/v3/api/store_type_models.go index e116024..56b5a5c 100644 --- a/v3/api/store_type_models.go +++ b/v3/api/store_type_models.go @@ -39,7 +39,7 @@ type CertificateStoreType struct { Capability string `json:"Capability,omitempty"` StoreType int `json:"StoreType"` ImportType int `json:"ImportType,omitempty"` - LocalStore bool `json:"LocalStore,omitempty"` + LocalStore *bool `json:"LocalStore,omitempty"` SupportedOperations *StoreTypeSupportedOperations `json:"SupportedOperations,omitempty"` Properties *[]StoreTypePropertyDefinition `json:"Properties,omitempty"` EntryParameters *[]EntryParameter `json:"EntryParameters,omitempty"` @@ -48,9 +48,9 @@ type CertificateStoreType struct { StorePathValue string `json:"StorePathValue,omitempty"` PrivateKeyAllowed string `json:"PrivateKeyAllowed,omitempty"` JobProperties *[]string `json:"JobProperties,omitempty"` - ServerRequired bool `json:"ServerRequired,omitempty"` - PowerShell bool `json:"PowerShell,omitempty"` - BlueprintAllowed bool `json:"BlueprintAllowed,omitempty"` + ServerRequired *bool `json:"ServerRequired,omitempty"` + PowerShell *bool `json:"PowerShell,omitempty"` + BlueprintAllowed *bool `json:"BlueprintAllowed,omitempty"` CustomAliasAllowed string `json:"CustomAliasAllowed,omitempty"` ServerRegistration int `json:"ServerRegistration,omitempty"` InventoryEndpoint string `json:"InventoryEndpoint,omitempty"` From 06609d71131f4fc1ca54f82d5845244425d8dc13 Mon Sep 17 00:00:00 2001 From: spb <1661003+spbsoluble@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:26:32 -0700 Subject: [PATCH 39/51] fix(template): add TemplatePolicy field to Templates request/response models (#57) --- v3/api/template_models.go | 41 ++++++++++++++ v3/api/template_test.go | 114 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+) diff --git a/v3/api/template_models.go b/v3/api/template_models.go index 526b8c4..127683b 100644 --- a/v3/api/template_models.go +++ b/v3/api/template_models.go @@ -35,6 +35,44 @@ type GetTemplateResponse struct { RFCEnforcement bool `json:"RFCEnforcement,omitempty"` RequiresApproval bool `json:"RequiresApproval,omitempty"` KeyUsage int `json:"KeyUsage,omitempty"` + // TemplatePolicy carries the template's key-algorithm policy (PrimaryKeyAlgorithms / + // AlternativeKeyAlgorithms, wildcard/key-reuse flags, certificate owner role, etc). + // It is null for templates that predate this policy model or have never had it + // configured. Command's PUT /Templates full-replace validation derives an internal + // "Policies" set from TemplatePolicy.PrimaryKeyAlgorithms/AlternativeKeyAlgorithms; + // for templates linked to an enrollment pattern, omitting TemplatePolicy on update + // collapses that set to empty and Command rejects the request with + // "'Policies' cannot be empty" (confirmed against a live Command 25.4.1 instance). + TemplatePolicy *TemplatePolicy `json:"TemplatePolicy,omitempty"` +} + +// TemplateKeyAlgorithm describes one allowed key algorithm entry within a +// TemplatePolicy's PrimaryKeyAlgorithms/AlternativeKeyAlgorithms list. Field +// names intentionally match Command's lowercase/snake_case wire format for +// this nested object (unlike the rest of the Templates API, which is +// PascalCase). +type TemplateKeyAlgorithm struct { + Name string `json:"name,omitempty"` + BitLengths []int `json:"bit_lengths,omitempty"` + Curves []string `json:"curves,omitempty"` +} + +// TemplatePolicy models Command's per-template key/enrollment policy object, +// returned under GetTemplateResponse.TemplatePolicy and required (when the +// template has one configured) on UpdateTemplateArg.TemplatePolicy to avoid +// Command's full-replace PUT /Templates clearing it. See the comment on +// GetTemplateResponse.TemplatePolicy for the "'Policies' cannot be empty" +// validation error this addresses. +type TemplatePolicy struct { + TemplateId int `json:"TemplateId,omitempty"` + AllowKeyReuse *bool `json:"AllowKeyReuse,omitempty"` + AllowWildcards *bool `json:"AllowWildcards,omitempty"` + RFCEnforcement *bool `json:"RFCEnforcement,omitempty"` + CertificateOwnerRole *int `json:"CertificateOwnerRole,omitempty"` + DefaultCertificateOwnerRoleId *int `json:"DefaultCertificateOwnerRoleId,omitempty"` + DefaultCertificateOwnerRoleName *string `json:"DefaultCertificateOwnerRoleName,omitempty"` + PrimaryKeyAlgorithms []TemplateKeyAlgorithm `json:"PrimaryKeyAlgorithms,omitempty"` + AlternativeKeyAlgorithms []TemplateKeyAlgorithm `json:"AlternativeKeyAlgorithms,omitempty"` } type TemplateEnrollmentFields struct { @@ -82,6 +120,9 @@ type UpdateTemplateArg struct { RFCEnforcement *bool `json:"RFCEnforcement,omitempty"` RequiresApproval *bool `json:"RequiresApproval,omitempty"` KeyUsage *bool `json:"KeyUsage,omitempty"` + // TemplatePolicy must be round-tripped from the corresponding GetTemplateResponse + // on every update; see the field comment on GetTemplateResponse.TemplatePolicy. + TemplatePolicy *TemplatePolicy `json:"TemplatePolicy,omitempty"` } type UpdateTemplateResponse struct{ GetTemplateResponse } diff --git a/v3/api/template_test.go b/v3/api/template_test.go index d6bc71a..c18c629 100644 --- a/v3/api/template_test.go +++ b/v3/api/template_test.go @@ -2,6 +2,7 @@ package api import ( "encoding/json" + "io" "net/http" "net/http/httptest" "strconv" @@ -130,3 +131,116 @@ func TestGetTemplates_SinglePage(t *testing.T) { t.Errorf("server called %d times, want 1", calls) } } + +// TestGetTemplateResponse_TemplatePolicy_Decode verifies that GetTemplateResponse +// decodes the "TemplatePolicy" object Command returns for templates linked to an +// enrollment pattern (PrimaryKeyAlgorithms/AlternativeKeyAlgorithms, wildcard/key-reuse +// flags, etc). Before the fix GetTemplateResponse had no TemplatePolicy field at all, +// so encoding/json silently dropped it and buildTemplateRoleBindingUpdateArg-style +// callers had nothing to copy forward. +func TestGetTemplateResponse_TemplatePolicy_Decode(t *testing.T) { + // Trimmed down, real shape captured from a live Command 25.4.1 GET /Templates/{id} + // response for a template linked to an enrollment pattern. + body := `{ + "Id": 4, + "CommonName": "Server_tlsServerAuth-1y", + "UseAllowedRequesters": true, + "AllowedRequesters": ["Administrator", "InstanceOwner"], + "TemplatePolicy": { + "TemplateId": 4, + "AllowKeyReuse": true, + "AllowWildcards": true, + "RFCEnforcement": null, + "CertificateOwnerRole": 0, + "PrimaryKeyAlgorithms": [ + {"name": "RSA", "bit_lengths": [2048, 3072, 4096], "curves": []}, + {"name": "Ed25519", "bit_lengths": [255], "curves": []} + ], + "AlternativeKeyAlgorithms": [] + } + }` + + var resp GetTemplateResponse + if err := json.Unmarshal([]byte(body), &resp); err != nil { + t.Fatalf("json.Unmarshal() error: %v", err) + } + + if resp.TemplatePolicy == nil { + t.Fatal("GetTemplateResponse.TemplatePolicy = nil, want non-nil (field not decoded)") + } + if got, want := len(resp.TemplatePolicy.PrimaryKeyAlgorithms), 2; got != want { + t.Fatalf("len(TemplatePolicy.PrimaryKeyAlgorithms) = %d, want %d", got, want) + } + if got, want := resp.TemplatePolicy.PrimaryKeyAlgorithms[0].Name, "RSA"; got != want { + t.Errorf("PrimaryKeyAlgorithms[0].Name = %q, want %q", got, want) + } + if got, want := resp.TemplatePolicy.PrimaryKeyAlgorithms[0].BitLengths, []int{2048, 3072, 4096}; len(got) != len(want) { + t.Errorf("PrimaryKeyAlgorithms[0].BitLengths = %v, want %v", got, want) + } + if resp.TemplatePolicy.AllowKeyReuse == nil || !*resp.TemplatePolicy.AllowKeyReuse { + t.Errorf("TemplatePolicy.AllowKeyReuse = %v, want true", resp.TemplatePolicy.AllowKeyReuse) + } +} + +// TestUpdateTemplateArg_TemplatePolicy_Roundtrip verifies that an UpdateTemplateArg +// with TemplatePolicy set actually serializes that field onto the wire when passed to +// UpdateTemplate. This is the crux of the "'Policies' cannot be empty" bug fix +// (Keyfactor/terraform-provider-keyfactor#180): Command's PUT /Templates rejects the +// full-replace update outright for templates linked to an enrollment pattern unless +// TemplatePolicy.PrimaryKeyAlgorithms/AlternativeKeyAlgorithms round-trip the +// previously-fetched values. Before the fix, UpdateTemplateArg had no TemplatePolicy +// field, so this could never be sent — this test would fail to compile against the +// pre-fix struct definition. +func TestUpdateTemplateArg_TemplatePolicy_Roundtrip(t *testing.T) { + var receivedBody []byte + + srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var err error + receivedBody, err = io.ReadAll(r.Body) + if err != nil { + t.Fatalf("failed to read request body: %v", err) + } + w.Header().Set("Content-Type", "application/json") + w.Write(receivedBody) + })) + defer srv.Close() + + c := newTestClient(srv) + + allowKeyReuse := true + allowWildcards := true + useAllowedRequesters := true + allowedRequesters := []string{"Administrator", "InstanceOwner"} + arg := &UpdateTemplateArg{ + Id: 4, + UseAllowedRequesters: &useAllowedRequesters, + AllowedRequesters: &allowedRequesters, + TemplatePolicy: &TemplatePolicy{ + TemplateId: 4, + AllowKeyReuse: &allowKeyReuse, + AllowWildcards: &allowWildcards, + PrimaryKeyAlgorithms: []TemplateKeyAlgorithm{ + {Name: "RSA", BitLengths: []int{2048, 3072, 4096}}, + {Name: "Ed25519", BitLengths: []int{255}}, + }, + }, + } + + if _, err := c.UpdateTemplate(arg); err != nil { + t.Fatalf("UpdateTemplate() error: %v", err) + } + + var onWire map[string]interface{} + if err := json.Unmarshal(receivedBody, &onWire); err != nil { + t.Fatalf("failed to decode request body sent to server: %v", err) + } + + policy, ok := onWire["TemplatePolicy"].(map[string]interface{}) + if !ok { + t.Fatalf("request body sent to server has no TemplatePolicy object; got keys: %v", onWire) + } + primaryAlgos, ok := policy["PrimaryKeyAlgorithms"].([]interface{}) + if !ok || len(primaryAlgos) != 2 { + t.Fatalf("TemplatePolicy.PrimaryKeyAlgorithms on the wire = %v, want 2 entries", policy["PrimaryKeyAlgorithms"]) + } +} From a3d995829e4bf46afc5183ed6af07a781db9b823 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 22 Jul 2026 14:21:48 -0700 Subject: [PATCH 40/51] fix(template): change UpdateTemplateArg.KeyUsage to *int to match Command API MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Command's TemplateUpdateRequest.KeyUsage and TemplateRetrievalResponse.KeyUsage are both {"type":"integer","format":"int32"} per the v25.5 swagger — an int32 bitmask (e.g. 160 = digitalSignature|keyEncipherment). UpdateTemplateArg.KeyUsage was typed *bool, which serializes as a JSON boolean and produces a live HTTP 400 from Command ("Unexpected character encountered while parsing value: t. Path 'KeyUsage'"), making the field unusable as-is. GetTemplateResponse.KeyUsage was already int, so this also fixes the type mismatch between the get and update models for the same field. Also fixes the identical defect in v2/api/template_models.go for consistency; v2 is tagged/released independently and is not part of this v3.6.0 change. Adds TestUpdateTemplateArg_KeyUsage_SerializesAsInt to v3/api/template_test.go, which fails to compile against the pre-fix *bool field and asserts the wire payload is a JSON number. --- v2/api/template_models.go | 8 +++++- v3/api/template_models.go | 8 +++++- v3/api/template_test.go | 59 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/v2/api/template_models.go b/v2/api/template_models.go index 526b8c4..70eefd7 100644 --- a/v2/api/template_models.go +++ b/v2/api/template_models.go @@ -81,7 +81,13 @@ type UpdateTemplateArg struct { AllowedRequesters *[]string `json:"AllowedRequesters,omitempty"` RFCEnforcement *bool `json:"RFCEnforcement,omitempty"` RequiresApproval *bool `json:"RequiresApproval,omitempty"` - KeyUsage *bool `json:"KeyUsage,omitempty"` + // KeyUsage is an int32 bitmask on Command's wire format (e.g. 160 = + // digitalSignature|keyEncipherment), matching GetTemplateResponse.KeyUsage and + // Command's TemplateUpdateRequest/TemplateRetrievalResponse swagger schema + // (both typed "integer"/"int32"). A *bool here previously produced a live + // HTTP 400 ("Unexpected character encountered while parsing value: t. Path + // 'KeyUsage'") since Command rejects a JSON boolean for an integer field. + KeyUsage *int `json:"KeyUsage,omitempty"` } type UpdateTemplateResponse struct{ GetTemplateResponse } diff --git a/v3/api/template_models.go b/v3/api/template_models.go index 127683b..39d905b 100644 --- a/v3/api/template_models.go +++ b/v3/api/template_models.go @@ -119,7 +119,13 @@ type UpdateTemplateArg struct { AllowedRequesters *[]string `json:"AllowedRequesters,omitempty"` RFCEnforcement *bool `json:"RFCEnforcement,omitempty"` RequiresApproval *bool `json:"RequiresApproval,omitempty"` - KeyUsage *bool `json:"KeyUsage,omitempty"` + // KeyUsage is an int32 bitmask on Command's wire format (e.g. 160 = + // digitalSignature|keyEncipherment), matching GetTemplateResponse.KeyUsage and + // Command's TemplateUpdateRequest/TemplateRetrievalResponse swagger schema + // (both typed "integer"/"int32"). A *bool here previously produced a live + // HTTP 400 ("Unexpected character encountered while parsing value: t. Path + // 'KeyUsage'") since Command rejects a JSON boolean for an integer field. + KeyUsage *int `json:"KeyUsage,omitempty"` // TemplatePolicy must be round-tripped from the corresponding GetTemplateResponse // on every update; see the field comment on GetTemplateResponse.TemplatePolicy. TemplatePolicy *TemplatePolicy `json:"TemplatePolicy,omitempty"` diff --git a/v3/api/template_test.go b/v3/api/template_test.go index c18c629..ee27197 100644 --- a/v3/api/template_test.go +++ b/v3/api/template_test.go @@ -244,3 +244,62 @@ func TestUpdateTemplateArg_TemplatePolicy_Roundtrip(t *testing.T) { t.Fatalf("TemplatePolicy.PrimaryKeyAlgorithms on the wire = %v, want 2 entries", policy["PrimaryKeyAlgorithms"]) } } + +// TestUpdateTemplateArg_KeyUsage_SerializesAsInt verifies that UpdateTemplateArg.KeyUsage +// serializes onto the wire as a JSON number, matching Command's TemplateUpdateRequest +// swagger schema ({"type":"integer","format":"int32"}) confirmed against a live v25.5 +// instance. Before the fix, KeyUsage was typed *bool, which serialized as a JSON boolean +// and produced a live HTTP 400 from Command ("Unexpected character encountered while +// parsing value: t. Path 'KeyUsage'"). This also verifies the value returned by +// GetTemplateResponse.KeyUsage (an int) can be assigned directly to +// UpdateTemplateArg.KeyUsage without a type conversion, since both now agree on int. +func TestUpdateTemplateArg_KeyUsage_SerializesAsInt(t *testing.T) { + var receivedBody []byte + + srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var err error + receivedBody, err = io.ReadAll(r.Body) + if err != nil { + t.Fatalf("failed to read request body: %v", err) + } + w.Header().Set("Content-Type", "application/json") + w.Write(receivedBody) + })) + defer srv.Close() + + c := newTestClient(srv) + + // Simulate a real read-modify-write: KeyUsage comes straight off a + // GetTemplateResponse (int) with no bool<->int conversion required. + fetched := GetTemplateResponse{Id: 4, KeyUsage: 160} // digitalSignature|keyEncipherment + keyUsage := fetched.KeyUsage + + arg := &UpdateTemplateArg{ + Id: 4, + KeyUsage: &keyUsage, + } + + if _, err := c.UpdateTemplate(arg); err != nil { + t.Fatalf("UpdateTemplate() error: %v", err) + } + + var onWire map[string]interface{} + if err := json.Unmarshal(receivedBody, &onWire); err != nil { + t.Fatalf("failed to decode request body sent to server: %v", err) + } + + rawKeyUsage, ok := onWire["KeyUsage"] + if !ok { + t.Fatalf("request body sent to server has no KeyUsage field; got keys: %v", onWire) + } + switch v := rawKeyUsage.(type) { + case float64: + if v != 160 { + t.Errorf("KeyUsage on the wire = %v, want 160", v) + } + case bool: + t.Fatalf("KeyUsage on the wire is a JSON boolean (%v); Command's API expects an int32 bitmask and returns HTTP 400 for a boolean payload", v) + default: + t.Fatalf("KeyUsage on the wire has unexpected type %T (value %v), want a JSON number", v, v) + } +} From e46009a31a94c27ea6e9392739106d63f566c4ca Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Thu, 13 Aug 2026 18:38:21 -0700 Subject: [PATCH 41/51] fix(client): plumb Server.ClientTimeout into rebuilt auth config NewKeyfactorClient rebuilds a fresh CommandAuthConfig from the caller's *auth_providers.Server instead of reusing the one that produced it, but never carried over ClientTimeout. Every consumer -- including the Terraform provider's request_timeout setting -- ended up authenticating and issuing requests with DefaultClientTimeout (60s) regardless of what was configured, causing "net/http: timeout awaiting response headers" on long-running calls like PFX enrollment. Set HttpClientTimeout: cfg.ClientTimeout in the baseConfig literal so it flows into BuildTransport()/SetClient() for both the basic and oauth auth paths. Depends on github.com/Keyfactor/keyfactor-auth-client-go#51 being fixed upstream (Server.ClientTimeout field). go.mod is bumped to the not-yet-tagged v1.6.0-rc.1 and pinned locally via a `replace` directive at /tmp/kf-worktrees/kfc-auth for testing; once that tag is cut, drop the replace and re-run `go mod tidy`. --- v3/api/client.go | 11 ++-- v3/api/client_test.go | 125 ++++++++++++++++++++++++++++++++++++++++++ v3/go.mod | 8 ++- v3/go.sum | 4 -- 4 files changed, 138 insertions(+), 10 deletions(-) create mode 100644 v3/api/client_test.go diff --git a/v3/api/client.go b/v3/api/client.go index add6a21..8be9d2c 100644 --- a/v3/api/client.go +++ b/v3/api/client.go @@ -142,11 +142,12 @@ func NewKeyfactorClient(cfg *auth_providers.Server, ctx *context.Context) (*Clie clientAuthType := cfg.GetAuthType() baseConfig := auth_providers.CommandAuthConfig{ - CommandHostName: cfg.Host, - CommandPort: cfg.Port, - CommandAPIPath: cfg.APIPath, - CommandCACert: cfg.CACertPath, - SkipVerify: cfg.SkipTLSVerify, + CommandHostName: cfg.Host, + CommandPort: cfg.Port, + CommandAPIPath: cfg.APIPath, + CommandCACert: cfg.CACertPath, + SkipVerify: cfg.SkipTLSVerify, + HttpClientTimeout: cfg.ClientTimeout, } if clientAuthType == "basic" { diff --git a/v3/api/client_test.go b/v3/api/client_test.go new file mode 100644 index 0000000..212ec35 --- /dev/null +++ b/v3/api/client_test.go @@ -0,0 +1,125 @@ +// Copyright 2024 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "context" + "net/http" + "net/http/httptest" + "net/url" + "testing" + "time" + + "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers" +) + +// newFakeCommandServer stands in for a Keyfactor Command instance for +// CommandAuthConfigBasic.Authenticate(), which performs a real GET against +// {host}/{apiPath}/Status/Endpoints as part of authentication. It always +// returns 200 with a valid JSON string array, regardless of credentials. +func newFakeCommandServer(t *testing.T) *httptest.Server { + t.Helper() + server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`["endpoint1"]`)) + })) + t.Cleanup(server.Close) + return server +} + +// TestNewKeyfactorClient_PlumbsClientTimeout is a regression test proving that +// a Server.ClientTimeout value survives NewKeyfactorClient's rebuild of the +// CommandAuthConfig. Before this fix, baseConfig never set HttpClientTimeout, +// so the rebuilt auth config (and everything derived from it, including +// BuildTransport's ResponseHeaderTimeout) silently fell back to +// DefaultClientTimeout (60s) regardless of what the caller configured, +// producing "net/http: timeout awaiting response headers" on long-running +// calls such as PFX enrollment. +func TestNewKeyfactorClient_PlumbsClientTimeout(t *testing.T) { + server := newFakeCommandServer(t) + u, uErr := url.Parse(server.URL) + if uErr != nil { + t.Fatalf("failed to parse test server URL: %v", uErr) + } + + cfg := &auth_providers.Server{ + Host: u.Host, + Username: "user", + Password: "pass", + APIPath: "api", + SkipTLSVerify: true, + ClientTimeout: 300, + } + + ctx := context.Background() + client, err := NewKeyfactorClient(cfg, &ctx) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + basicCfg, ok := client.AuthClient.(*auth_providers.CommandAuthConfigBasic) + if !ok { + t.Fatalf("expected AuthClient to be *auth_providers.CommandAuthConfigBasic, got %T", client.AuthClient) + } + + if basicCfg.HttpClientTimeout != 300 { + t.Fatalf("expected HttpClientTimeout to be 300, got %d", basicCfg.HttpClientTimeout) + } + + transport, tErr := basicCfg.CommandAuthConfig.BuildTransport() + if tErr != nil { + t.Fatalf("expected no error building transport, got %v", tErr) + } + + expected := 300 * time.Second + if transport.ResponseHeaderTimeout != expected { + t.Fatalf("expected ResponseHeaderTimeout to be %v, got %v", expected, transport.ResponseHeaderTimeout) + } +} + +// TestNewKeyfactorClient_DefaultClientTimeout confirms the zero-value +// (unset) case still falls back to the library default rather than 0s, +// preserving pre-fix behavior for callers who don't set ClientTimeout. +func TestNewKeyfactorClient_DefaultClientTimeout(t *testing.T) { + server := newFakeCommandServer(t) + u, uErr := url.Parse(server.URL) + if uErr != nil { + t.Fatalf("failed to parse test server URL: %v", uErr) + } + + cfg := &auth_providers.Server{ + Host: u.Host, + Username: "user", + Password: "pass", + APIPath: "api", + SkipTLSVerify: true, + } + + ctx := context.Background() + client, err := NewKeyfactorClient(cfg, &ctx) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + basicCfg, ok := client.AuthClient.(*auth_providers.CommandAuthConfigBasic) + if !ok { + t.Fatalf("expected AuthClient to be *auth_providers.CommandAuthConfigBasic, got %T", client.AuthClient) + } + + if basicCfg.HttpClientTimeout != auth_providers.DefaultClientTimeout { + t.Fatalf("expected HttpClientTimeout to fall back to default %d, got %d", auth_providers.DefaultClientTimeout, basicCfg.HttpClientTimeout) + } +} diff --git a/v3/go.mod b/v3/go.mod index f4eb047..1e61246 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -19,7 +19,11 @@ go 1.24.0 toolchain go1.24.5 require ( - github.com/Keyfactor/keyfactor-auth-client-go v1.5.0 + // TODO(fix/server-client-timeout): bump to v1.6.0-rc.1 once that tag is cut + // upstream (fixes Server.ClientTimeout plumbing, see + // https://github.com/Keyfactor/keyfactor-auth-client-go/issues/51), then + // remove the local `replace` below and re-run `go mod tidy`. + github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.1 github.com/hashicorp/terraform-plugin-log v0.10.0 github.com/spbsoluble/go-pkcs12 v0.4.0 github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 @@ -55,3 +59,5 @@ require ( golang.org/x/text v0.33.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) + +replace github.com/Keyfactor/keyfactor-auth-client-go => /tmp/kf-worktrees/kfc-auth diff --git a/v3/go.sum b/v3/go.sum index 451aad8..6b9ffa5 100644 --- a/v3/go.sum +++ b/v3/go.sum @@ -14,8 +14,6 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= -github.com/Keyfactor/keyfactor-auth-client-go v1.5.0 h1:sq7SGkJeTtDspFSuX2oJxTmFiiFfaQ68B4JP7jryl94= -github.com/Keyfactor/keyfactor-auth-client-go v1.5.0/go.mod h1:rFBZPMSHWwWuUwE1kXhLsDaOxjGiHMbXTTEni8Dmufo= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -75,8 +73,6 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/spbsoluble/go-pkcs12 v0.3.3 h1:3nh7IKn16RDpmrSMtOu1JvbB0XHYq1j+IsICdU1c7J4= -github.com/spbsoluble/go-pkcs12 v0.3.3/go.mod h1:MAxKIUEIl/QVcua/I1L4Otyxl9UvLCCIktce2Tjz6Nw= github.com/spbsoluble/go-pkcs12 v0.4.0 h1:3HOVPZ8pvYqhAyz/NJzT9YODQJ3HbZvB9/CMVmvGaUM= github.com/spbsoluble/go-pkcs12 v0.4.0/go.mod h1:MAxKIUEIl/QVcua/I1L4Otyxl9UvLCCIktce2Tjz6Nw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= From 4ef0c0a530d5acc0bd4ee00f3551c51f9d549689 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Fri, 14 Aug 2026 08:08:32 -0700 Subject: [PATCH 42/51] chore(deps): bump keyfactor-auth-client-go to v1.6.0-rc.2 Removes the local replace directive and TODO now that the ClientTimeout fix is published, and validates against the published dependency. --- v3/go.mod | 8 +------- v3/go.sum | 2 ++ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/v3/go.mod b/v3/go.mod index 1e61246..e061bc3 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -19,11 +19,7 @@ go 1.24.0 toolchain go1.24.5 require ( - // TODO(fix/server-client-timeout): bump to v1.6.0-rc.1 once that tag is cut - // upstream (fixes Server.ClientTimeout plumbing, see - // https://github.com/Keyfactor/keyfactor-auth-client-go/issues/51), then - // remove the local `replace` below and re-run `go mod tidy`. - github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.1 + github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.2 github.com/hashicorp/terraform-plugin-log v0.10.0 github.com/spbsoluble/go-pkcs12 v0.4.0 github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 @@ -59,5 +55,3 @@ require ( golang.org/x/text v0.33.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect ) - -replace github.com/Keyfactor/keyfactor-auth-client-go => /tmp/kf-worktrees/kfc-auth diff --git a/v3/go.sum b/v3/go.sum index 6b9ffa5..670986e 100644 --- a/v3/go.sum +++ b/v3/go.sum @@ -14,6 +14,8 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= +github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.2 h1:wp7LBuNSpHZYPlzEuipNeuWwwBow8lgLj8lD2gMivhM= +github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.2/go.mod h1:rFBZPMSHWwWuUwE1kXhLsDaOxjGiHMbXTTEni8Dmufo= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= From 40a77564924281fc5c40e5f7d7ecbb97e5c26927 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Fri, 14 Aug 2026 08:55:21 -0700 Subject: [PATCH 43/51] fix(client): cache and reuse http client across requests Client.sendRequest called AuthConfig.GetHttpClient() on every single request. Both CommandConfigOauth and CommandAuthConfigBasic in keyfactor-auth-client-go build a brand new http.Transport (and therefore a brand new, empty connection pool) on each call, and that transport's IdleConnTimeout is derived from the configured HttpClientTimeout - so every API call opened its own never-reused connection whose socket lingered until IdleConnTimeout fired. This leak predates this branch at the fixed 60s default; plumbing a caller-configured ClientTimeout through (which can be arbitrarily large, e.g. 1800s for slow enrollments) widens the linger window proportionally, so cache the *http.Client on Client and reuse it across requests instead of rebuilding it per call. The OAuth token source is still consulted (and refreshed) on every RoundTrip independent of how many times the *http.Client is reused, and NewKeyfactorClientWithAuth (used by VCR/unit tests) still works by lazily populating the cache on first use. --- v3/api/client.go | 50 +++++++++++++++++-- v3/api/client_test.go | 108 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 155 insertions(+), 3 deletions(-) diff --git a/v3/api/client.go b/v3/api/client.go index 8be9d2c..b8c63d0 100644 --- a/v3/api/client.go +++ b/v3/api/client.go @@ -28,6 +28,7 @@ import ( "net/url" "path" "strings" + "sync" "time" "github.com/Keyfactor/keyfactor-auth-client-go/auth_providers" @@ -69,6 +70,47 @@ var ( type Client struct { AuthClient AuthConfig LoggerType string + + // httpClient caches the *http.Client returned by AuthClient.GetHttpClient() + // so that sendRequest reuses a single underlying transport/connection pool + // across requests instead of asking AuthClient to build a brand new one on + // every call. Both CommandConfigOauth.GetHttpClient() and + // CommandAuthConfigBasic.GetHttpClient() (in keyfactor-auth-client-go) + // construct a fresh http.Transport per invocation, and that transport's + // IdleConnTimeout is derived from the configured HttpClientTimeout - so + // without this cache, every request opens its own connection pool whose + // sockets linger for up to HttpClientTimeout before being reclaimed. This + // was already true at the old fixed 60s default; plumbing a caller-supplied + // ClientTimeout (see NewKeyfactorClient) just widens the window, so caching + // here keeps that fix from amplifying a pre-existing resource leak. + httpClient *http.Client + httpClientMu sync.Mutex +} + +// getHttpClient returns the cached *http.Client if one has already been +// resolved for this Client, populating the cache on first use otherwise. +// This guarantees AuthClient.GetHttpClient() is invoked at most once per +// Client instance, so the transport (and its connection pool) is reused +// across requests. It is safe for concurrent use. +// +// Note this does not affect OAuth token refresh: the cached *http.Client's +// transport wraps an oauth2 TokenSource that is consulted (and refreshed as +// needed) on every RoundTrip, independent of how many times the *http.Client +// itself is reused. +func (c *Client) getHttpClient() (*http.Client, error) { + c.httpClientMu.Lock() + defer c.httpClientMu.Unlock() + + if c.httpClient != nil { + return c.httpClient, nil + } + + httpClient, err := c.AuthClient.GetHttpClient() + if err != nil { + return nil, err + } + c.httpClient = httpClient + return httpClient, nil } // TerraformLogger wraps the tflog logging to handle Go's log messages with log level mapping. @@ -161,11 +203,12 @@ func NewKeyfactorClient(cfg *auth_providers.Server, ctx *context.Context) (*Clie if aErr != nil { return nil, aErr } - _, cErr := basicCfg.GetHttpClient() + httpClient, cErr := basicCfg.GetHttpClient() if cErr != nil { return nil, cErr } client.AuthClient = &basicCfg + client.httpClient = httpClient return &client, nil } else if clientAuthType == "oauth" { oauthCfg := auth_providers.CommandConfigOauth{ @@ -181,11 +224,12 @@ func NewKeyfactorClient(cfg *auth_providers.Server, ctx *context.Context) (*Clie if aErr != nil { return nil, aErr } - _, cErr := oauthCfg.GetHttpClient() + httpClient, cErr := oauthCfg.GetHttpClient() if cErr != nil { return nil, cErr } client.AuthClient = &oauthCfg + client.httpClient = httpClient return &client, nil } else { return nil, fmt.Errorf("unsupported auth type or authentication cfg: '%s'", clientAuthType) @@ -343,7 +387,7 @@ func (c *Client) sendRequest(request *request) (*http.Response, error) { // Log the request logRequest(req) - httpClient, cErr := c.AuthClient.GetHttpClient() + httpClient, cErr := c.getHttpClient() if cErr != nil { return nil, cErr } diff --git a/v3/api/client_test.go b/v3/api/client_test.go index 212ec35..f7123b0 100644 --- a/v3/api/client_test.go +++ b/v3/api/client_test.go @@ -16,9 +16,13 @@ package api import ( "context" + "crypto/tls" + "io" + "net" "net/http" "net/http/httptest" "net/url" + "sync/atomic" "testing" "time" @@ -123,3 +127,107 @@ func TestNewKeyfactorClient_DefaultClientTimeout(t *testing.T) { t.Fatalf("expected HttpClientTimeout to fall back to default %d, got %d", auth_providers.DefaultClientTimeout, basicCfg.HttpClientTimeout) } } + +// perCallTransportAuthConfig is a minimal AuthConfig test double that mimics +// the real behavior of keyfactor-auth-client-go's CommandConfigOauth and +// CommandAuthConfigBasic GetHttpClient() implementations: every call builds a +// brand new *http.Transport (and therefore a brand new, empty connection +// pool) rather than reusing one. It exists to prove that Client caches the +// *http.Client it gets back rather than calling GetHttpClient() (and paying +// for a fresh transport/connection pool) on every request. +type perCallTransportAuthConfig struct { + server *httptest.Server + getClientCalls int32 +} + +func (a *perCallTransportAuthConfig) GetServerConfig() *auth_providers.Server { + return &auth_providers.Server{ + Host: a.server.URL, + APIPath: "KeyfactorAPI", + SkipTLSVerify: true, + } +} + +func (a *perCallTransportAuthConfig) GetHttpClient() (*http.Client, error) { + atomic.AddInt32(&a.getClientCalls, 1) + return &http.Client{ + Transport: &http.Transport{ + TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, + }, + }, nil +} + +func (a *perCallTransportAuthConfig) Authenticate() error { return nil } + +func (a *perCallTransportAuthConfig) GetCommandVersion() string { return "25.1.0.0" } + +// TestClient_ReusesHttpClientAcrossRequests is a regression test for a +// resource leak: sendRequest used to call c.AuthClient.GetHttpClient() on +// every single request. Since the real AuthConfig implementations build a +// brand new http.Transport (and connection pool) per call, and that +// transport's IdleConnTimeout is derived from the configured +// HttpClientTimeout, every API call opened its own never-reused connection +// whose socket lingered until IdleConnTimeout fired - amplified by the fix +// that plumbs a caller-configured ClientTimeout (which can be arbitrarily +// large, e.g. 1800s) all the way through instead of the fixed 60s default. +// +// This test drives Client.sendRequest directly across multiple requests and +// asserts both that AuthConfig.GetHttpClient() is invoked at most once +// (proving the *http.Client is cached) and that the underlying TCP +// connection is reused rather than growing linearly with the request count. +func TestClient_ReusesHttpClientAcrossRequests(t *testing.T) { + var newConns int32 + srv := httptest.NewUnstartedServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`[]`)) + }, + ), + ) + srv.Config.ConnState = func(_ net.Conn, state http.ConnState) { + if state == http.StateNew { + atomic.AddInt32(&newConns, 1) + } + } + srv.StartTLS() + t.Cleanup(srv.Close) + + auth := &perCallTransportAuthConfig{server: srv} + client := NewKeyfactorClientWithAuth(auth, nil) + + const requestCount = 10 + for i := 0; i < requestCount; i++ { + resp, err := client.sendRequest( + &request{ + Method: http.MethodGet, + Endpoint: "CertificateStoreContainers", + Headers: &apiHeaders{}, + }, + ) + if err != nil { + t.Fatalf("request %d failed: %v", i, err) + } + // Fully drain and close the body so the underlying transport is free + // to return the connection to its idle pool for reuse. + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + } + + if calls := atomic.LoadInt32(&auth.getClientCalls); calls != 1 { + t.Fatalf( + "expected AuthClient.GetHttpClient to be called exactly once across %d requests (client should be cached), got %d calls", + requestCount, + calls, + ) + } + + if conns := atomic.LoadInt32(&newConns); conns > 2 { + t.Fatalf( + "expected the TCP connection to be reused across %d sequential requests (at most ~1-2 new connections), observed %d new connections", + requestCount, + conns, + ) + } +} From 9aa4970c67d4958bf21a708ad48a6d5eef4d6686 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Fri, 14 Aug 2026 08:56:09 -0700 Subject: [PATCH 44/51] test(client): isolate ambient KEYFACTOR_* env vars in client tests TestNewKeyfactorClient_PlumbsClientTimeout and TestNewKeyfactorClient_DefaultClientTimeout build a Server config with fields intentionally left at their zero value to exercise ValidateAuthConfig's environment-variable fallback path. Because ValidateAuthConfig only falls back to KEYFACTOR_CLIENT_TIMEOUT/ KEYFACTOR_PORT/KEYFACTOR_CA_CERT when the struct field is unset, and unconditionally overwrites SkipVerify from KEYFACTOR_SKIP_VERIFY regardless of the struct field, ambient values for these variables (e.g. from a sourced lab env file) broke both tests: KEYFACTOR_CLIENT_TIMEOUT=120 flips the expected default from 60 to 120, and KEYFACTOR_SKIP_VERIFY=false clobbers SkipTLSVerify:true and rejects the tests' self-signed httptest TLS certificate. Add isolateKeyfactorEnv to unset the relevant variables for the duration of each test and restore their original values afterward. t.Setenv(key, "") does not work here since an empty value is still "present" to os.LookupEnv. --- v3/api/client_test.go | 51 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/v3/api/client_test.go b/v3/api/client_test.go index f7123b0..0bbc14e 100644 --- a/v3/api/client_test.go +++ b/v3/api/client_test.go @@ -22,6 +22,7 @@ import ( "net/http" "net/http/httptest" "net/url" + "os" "sync/atomic" "testing" "time" @@ -44,6 +45,42 @@ func newFakeCommandServer(t *testing.T) *httptest.Server { return server } +// isolateKeyfactorEnv unsets ambient KEYFACTOR_* environment variables that +// CommandAuthConfig.ValidateAuthConfig() falls back to whenever the +// corresponding struct field is left at its zero value, restoring their +// original values (present-and-unset, or present-with-value) once the test +// completes. This makes tests that build a Server/CommandAuthConfig with an +// intentionally-zero field (e.g. ClientTimeout: 0 to exercise the "use the +// default" path, or SkipTLSVerify relying on a literal true) hermetic: +// without this, a developer or CI job with KEYFACTOR_CLIENT_TIMEOUT or +// KEYFACTOR_SKIP_VERIFY exported in their shell would get spurious failures +// or, worse, a silently-clobbered SkipVerify that rejects the test's +// self-signed httptest TLS cert. +// +// Note: t.Setenv(key, "") is NOT equivalent to unsetting - os.LookupEnv still +// reports the variable as present with an empty value, which is enough to +// take the "environment variable is set" branch in ValidateAuthConfig (e.g. +// strconv.Atoi("") fails silently and leaves HttpClientTimeout at 0 rather +// than falling through to DefaultClientTimeout). The variable must be +// actually removed from the environment. +func isolateKeyfactorEnv(t *testing.T, keys ...string) { + t.Helper() + for _, key := range keys { + key := key + originalValue, wasSet := os.LookupEnv(key) + if err := os.Unsetenv(key); err != nil { + t.Fatalf("failed to unset %s: %v", key, err) + } + t.Cleanup(func() { + if wasSet { + _ = os.Setenv(key, originalValue) + } else { + _ = os.Unsetenv(key) + } + }) + } +} + // TestNewKeyfactorClient_PlumbsClientTimeout is a regression test proving that // a Server.ClientTimeout value survives NewKeyfactorClient's rebuild of the // CommandAuthConfig. Before this fix, baseConfig never set HttpClientTimeout, @@ -53,6 +90,13 @@ func newFakeCommandServer(t *testing.T) *httptest.Server { // producing "net/http: timeout awaiting response headers" on long-running // calls such as PFX enrollment. func TestNewKeyfactorClient_PlumbsClientTimeout(t *testing.T) { + isolateKeyfactorEnv( + t, + auth_providers.EnvKeyfactorClientTimeout, + auth_providers.EnvKeyfactorSkipVerify, + auth_providers.EnvKeyfactorPort, + auth_providers.EnvKeyfactorCACert, + ) server := newFakeCommandServer(t) u, uErr := url.Parse(server.URL) if uErr != nil { @@ -98,6 +142,13 @@ func TestNewKeyfactorClient_PlumbsClientTimeout(t *testing.T) { // (unset) case still falls back to the library default rather than 0s, // preserving pre-fix behavior for callers who don't set ClientTimeout. func TestNewKeyfactorClient_DefaultClientTimeout(t *testing.T) { + isolateKeyfactorEnv( + t, + auth_providers.EnvKeyfactorClientTimeout, + auth_providers.EnvKeyfactorSkipVerify, + auth_providers.EnvKeyfactorPort, + auth_providers.EnvKeyfactorCACert, + ) server := newFakeCommandServer(t) u, uErr := url.Parse(server.URL) if uErr != nil { From 62dcb4958aa1b6f598c7cb1117babe437ba26dc9 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Fri, 14 Aug 2026 10:19:40 -0700 Subject: [PATCH 45/51] chore(deps): bump keyfactor-auth-client-go to v1.6.0-rc.3 Picks up the round-4 convergence fixes: ClientTimeout persistence gated across all three concrete auth types via delegation to the base type, a BOM-prefix bypass fix in nested-JSON secret redaction, MaxConnsPerHost widened to unbounded, and body redaction extended to cover JSON-in-string values. --- v3/go.mod | 2 +- v3/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/v3/go.mod b/v3/go.mod index e061bc3..b5760c4 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -19,7 +19,7 @@ go 1.24.0 toolchain go1.24.5 require ( - github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.2 + github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.3 github.com/hashicorp/terraform-plugin-log v0.10.0 github.com/spbsoluble/go-pkcs12 v0.4.0 github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 diff --git a/v3/go.sum b/v3/go.sum index 670986e..93efff2 100644 --- a/v3/go.sum +++ b/v3/go.sum @@ -14,8 +14,8 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= -github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.2 h1:wp7LBuNSpHZYPlzEuipNeuWwwBow8lgLj8lD2gMivhM= -github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.2/go.mod h1:rFBZPMSHWwWuUwE1kXhLsDaOxjGiHMbXTTEni8Dmufo= +github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.3 h1:1j0ZVOmay13SrpQkXrfaGBml8pEAsE7sDJzHHK6C2+Y= +github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.3/go.mod h1:rFBZPMSHWwWuUwE1kXhLsDaOxjGiHMbXTTEni8Dmufo= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= From e157379e01e1fd81a09b924ef5257910e77c84da Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Fri, 14 Aug 2026 10:19:51 -0700 Subject: [PATCH 46/51] test(client): verify concurrent requests are not capped at MaxConnsPerHost=10 Closes the loop on a finding this package's own http.Client-caching fix could not verify end-to-end: caching a single *http.Client turns the transport's MaxConnsPerHost into a permanent, unqueued-timeout concurrency ceiling for the process, since the cached client has no Timeout and requests carry no deadline. keyfactor-auth-client-go's fix (MaxConnsPerHost widened from a hardcoded 10 to unbounded) was only verified there by inspecting the constructed transport's field value. Add an end-to-end regression test that builds a real Client via NewKeyfactorClient, retrieves its cached *http.Client, and drives 25 concurrent requests through it against a real httptest server, asserting the server observes well more than 10 requests in flight at once. Confirmed this fails against v1.6.0-rc.2 (10 in-flight, ~620ms) and passes against v1.6.0-rc.3 (25 in-flight, ~225ms). --- v3/api/client_test.go | 167 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 167 insertions(+) diff --git a/v3/api/client_test.go b/v3/api/client_test.go index 0bbc14e..f256d40 100644 --- a/v3/api/client_test.go +++ b/v3/api/client_test.go @@ -23,6 +23,7 @@ import ( "net/http/httptest" "net/url" "os" + "sync" "sync/atomic" "testing" "time" @@ -282,3 +283,169 @@ func TestClient_ReusesHttpClientAcrossRequests(t *testing.T) { ) } } + +// TestClient_ConcurrentRequestsNotCappedByMaxConnsPerHost is an end-to-end +// regression test closing the loop on a finding this package's own +// http.Client-caching fix (see TestClient_ReusesHttpClientAcrossRequests) +// caused but could not fix locally: caching a single *http.Client means the +// transport keyfactor-auth-client-go builds is now reused for the lifetime +// of the Client instance, so any nonzero MaxConnsPerHost on that transport +// stops being a harmless per-request default and becomes a permanent, +// unqueued-timeout ceiling on concurrent in-flight requests for the whole +// process - e.g. `terraform apply -parallelism=25` would silently serialize +// into batches of N with no bound on how long excess requests wait, since +// neither the cached client's Timeout (0, unset) nor its requests' contexts +// impose one. +// +// keyfactor-auth-client-go previously hardcoded MaxConnsPerHost: 10 on this +// transport. Its own fix (auth_core.go's newHTTPTransport, now pinned at 0 / +// unbounded to match net/http.DefaultTransport) was verified from that +// repo's side by inspecting the constructed *http.Transport's field value. +// This test verifies the fix end-to-end from this repo's perspective instead +// of trusting that inspection alone: it builds a real Client via +// NewKeyfactorClient (exactly as production code does), retrieves its cached +// *http.Client via getHttpClient(), and drives 25 concurrent requests through +// it against a real httptest server, asserting the server actually observes +// well more than 10 requests in flight at once rather than serializing into +// batches of 10. +func TestClient_ConcurrentRequestsNotCappedByMaxConnsPerHost(t *testing.T) { + const ( + concurrentRequests = 25 + holdDuration = 200 * time.Millisecond + ) + + var ( + mu sync.Mutex + inFlight int + maxInFlight int + ) + + srv := httptest.NewTLSServer( + http.HandlerFunc( + func(w http.ResponseWriter, r *http.Request) { + // The initial CommandAuthConfigBasic.Authenticate() call made + // by NewKeyfactorClient below hits this same handler; letting + // it fall through the same slow path is harmless since it + // happens once, sequentially, before the concurrent phase + // starts timing anything. + mu.Lock() + inFlight++ + if inFlight > maxInFlight { + maxInFlight = inFlight + } + mu.Unlock() + + time.Sleep(holdDuration) + + mu.Lock() + inFlight-- + mu.Unlock() + + w.Header().Set("Content-Type", "application/json") + w.Header().Set("x-keyfactor-product-version", "99.9.9") + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte(`["endpoint1"]`)) + }, + ), + ) + t.Cleanup(srv.Close) + + u, uErr := url.Parse(srv.URL) + if uErr != nil { + t.Fatalf("failed to parse test server URL: %v", uErr) + } + + cfg := &auth_providers.Server{ + Host: u.Host, + Username: "user", + Password: "pass", + APIPath: "api", + SkipTLSVerify: true, + } + + ctx := context.Background() + client, err := NewKeyfactorClient(cfg, &ctx) + if err != nil { + t.Fatalf("NewKeyfactorClient failed: %v", err) + } + + httpClient, hErr := client.getHttpClient() + if hErr != nil { + t.Fatalf("getHttpClient failed: %v", hErr) + } + + // Reset the counters: the single sequential Authenticate() call above + // already touched inFlight/maxInFlight and this resets the baseline so + // the assertion below reflects only the concurrent phase. + mu.Lock() + inFlight = 0 + maxInFlight = 0 + mu.Unlock() + + start := time.Now() + var wg sync.WaitGroup + for i := 0; i < concurrentRequests; i++ { + wg.Add(1) + go func() { + defer wg.Done() + req, rErr := http.NewRequest(http.MethodGet, srv.URL+"/KeyfactorAPI/concurrent-probe", nil) + if rErr != nil { + t.Errorf("failed to build request: %v", rErr) + return + } + resp, dErr := httpClient.Do(req) + if dErr != nil { + t.Errorf("request failed: %v", dErr) + return + } + _ = resp.Body.Close() + }() + } + wg.Wait() + elapsed := time.Since(start) + + mu.Lock() + observedMax := maxInFlight + mu.Unlock() + + t.Logf( + "observed max in-flight requests: %d/%d, wall time: %s (old MaxConnsPerHost=10 cap measured ~10 in-flight/909ms for a comparable batch; unbounded measured ~315ms)", + observedMax, + concurrentRequests, + elapsed, + ) + + // The old hardcoded MaxConnsPerHost: 10 would cap this at exactly 10 + // no matter how many requests are fired concurrently. Assert well above + // that ceiling (comfortably below concurrentRequests to tolerate + // scheduler jitter) to prove the requests are not being serialized into + // batches of 10. + const minAcceptableMaxInFlight = 15 + if observedMax <= 10 { + t.Fatalf( + "expected max concurrent in-flight requests to exceed the old MaxConnsPerHost=10 ceiling, got %d (elapsed %s) - concurrency ceiling regression", + observedMax, + elapsed, + ) + } + if observedMax < minAcceptableMaxInFlight { + t.Fatalf( + "expected max concurrent in-flight requests to be close to %d (unbounded), got only %d (elapsed %s)", + concurrentRequests, + observedMax, + elapsed, + ) + } + + // Wall time is a secondary signal: fully serialized into batches of 10 + // would take ceil(25/10)*holdDuration ~= 3*200ms = 600ms; unbounded + // concurrency should complete in roughly one holdDuration plus overhead. + maxAcceptableElapsed := holdDuration * 2 + if elapsed > maxAcceptableElapsed { + t.Fatalf( + "expected wall time close to a single %s hold duration for unbounded concurrency, got %s (elapsed too long, suggests serialization)", + holdDuration, + elapsed, + ) + } +} From 95b7380701f5ca1754cc2caf63dee90cc87a98ce Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Mon, 17 Aug 2026 13:44:24 -0700 Subject: [PATCH 47/51] chore(deps): bump keyfactor-auth-client-go to v1.6.0-rc.4 Picks up the round 5-6 OAuth token-fetch timeout hardening (bounded TCP dial phase and overall call during Configure), discovered via live-lab investigation after the branch had already converged once. No public API surface used by this module changed. --- v3/go.mod | 2 +- v3/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/v3/go.mod b/v3/go.mod index b5760c4..e0da278 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -19,7 +19,7 @@ go 1.24.0 toolchain go1.24.5 require ( - github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.3 + github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.4 github.com/hashicorp/terraform-plugin-log v0.10.0 github.com/spbsoluble/go-pkcs12 v0.4.0 github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 diff --git a/v3/go.sum b/v3/go.sum index 93efff2..2adfd56 100644 --- a/v3/go.sum +++ b/v3/go.sum @@ -14,8 +14,8 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= -github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.3 h1:1j0ZVOmay13SrpQkXrfaGBml8pEAsE7sDJzHHK6C2+Y= -github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.3/go.mod h1:rFBZPMSHWwWuUwE1kXhLsDaOxjGiHMbXTTEni8Dmufo= +github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.4 h1:pDKfmVk74gRjwtqtz7khMMM+sc6SxQAfcla+f7Q3ZuY= +github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.4/go.mod h1:rFBZPMSHWwWuUwE1kXhLsDaOxjGiHMbXTTEni8Dmufo= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= From 6079971b7bd3f9dee6e9d48042cc0adc5b2b3a9c Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Mon, 17 Aug 2026 15:09:13 -0700 Subject: [PATCH 48/51] chore(deps): bump keyfactor-auth-client-go to v1.6.0-rc.5 Picks up the OAuth client_credentials token-fetch fix: avoid a redundant double round trip from AuthStyle probing and share a single deadline across retry attempts instead of a fresh timeout budget per attempt. --- v3/go.mod | 2 +- v3/go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/v3/go.mod b/v3/go.mod index e0da278..705858a 100644 --- a/v3/go.mod +++ b/v3/go.mod @@ -19,7 +19,7 @@ go 1.24.0 toolchain go1.24.5 require ( - github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.4 + github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.5 github.com/hashicorp/terraform-plugin-log v0.10.0 github.com/spbsoluble/go-pkcs12 v0.4.0 github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 diff --git a/v3/go.sum b/v3/go.sum index 2adfd56..df302cb 100644 --- a/v3/go.sum +++ b/v3/go.sum @@ -14,8 +14,8 @@ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1 h1:WJ github.com/AzureAD/microsoft-authentication-extensions-for-go/cache v0.1.1/go.mod h1:tCcJZ0uHAmvjsVYzEFivsRTN00oz5BEsRgQHu5JZ9WE= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0 h1:XRzhVemXdgvJqCH0sFfrBUTnUJSBrBf7++ypk+twtRs= github.com/AzureAD/microsoft-authentication-library-for-go v1.6.0/go.mod h1:HKpQxkWaGLJ+D/5H8QRpyQXA1eKjxkFlOMwck5+33Jk= -github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.4 h1:pDKfmVk74gRjwtqtz7khMMM+sc6SxQAfcla+f7Q3ZuY= -github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.4/go.mod h1:rFBZPMSHWwWuUwE1kXhLsDaOxjGiHMbXTTEni8Dmufo= +github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.5 h1:nsp5hrG7EtGFOAaAIyRHt7FSLSWJSIDz66GrLaJU4yA= +github.com/Keyfactor/keyfactor-auth-client-go v1.6.0-rc.5/go.mod h1:rFBZPMSHWwWuUwE1kXhLsDaOxjGiHMbXTTEni8Dmufo= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= From 7e94fa1ee1eab6244436f8f31f98057d79197b53 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 26 Aug 2026 08:16:43 -0700 Subject: [PATCH 49/51] fix(client): stop silently retrying/masking context-deadline timeouts in sendRequest sendRequest's context-deadline-exceeded handling had two confirmed HIGH-severity problems: 1. It transparently retried the request (up to 5 times with exponential backoff) and, if a retry succeeded, returned that success with no indication a timeout ever occurred. Callers that specifically need to detect a client-side-timeout-shaped error (e.g. terraform-provider-keyfactor's orphaned-PFX-enrollment recovery, which matches on "context deadline exceeded" to search for a resource that may have already been created server-side) never saw the error, so their recovery logic never ran. Worse, blindly retrying a non-idempotent request (e.g. a POST enrollment) risks creating a second server-side resource if the original request actually succeeded after the client gave up on it. 2. If every retry also failed, the response variable was never reassigned from its initial nil value and the switch had no return for this case, so execution fell through to `resp.StatusCode` on a nil *http.Response -- panicking the calling process (e.g. crashing `terraform apply` outright). Removes the silent retry-and-mask behavior entirely; a context-deadline error is now returned immediately and untouched, like any other transport error, eliminating both the masking and the nil-deref fall-through. Callers that need retry-with-backoff around a timeout (and know their request is safe to repeat) should implement that at their own call site. Adds regression tests reproducing both original failure modes: a request that times out on the first attempt but would have succeeded on a retry (now returns the timeout error instead of a masked success), and a request that never succeeds within the client's timeout (now returns a clean error instead of panicking). --- v3/api/client.go | 53 ++++++---------- v3/api/client_test.go | 139 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+), 36 deletions(-) diff --git a/v3/api/client.go b/v3/api/client.go index b8c63d0..c1b0eca 100644 --- a/v3/api/client.go +++ b/v3/api/client.go @@ -393,43 +393,24 @@ func (c *Client) sendRequest(request *request) (*http.Response, error) { } resp, respErr := httpClient.Do(req) - // check if context deadline exceeded + // NOTE: this used to silently retry on "context deadline exceeded" (up to + // MAX_CONTEXT_DEADLINE_RETRIES times) without ever surfacing that a retry + // happened. That's unsafe for two reasons: + // 1. Retrying a non-idempotent request (e.g. a POST enrollment) after a + // client-side timeout risks creating a second server-side resource + // if the original request actually succeeded after the client gave + // up on it -- exactly the scenario callers need to detect via the + // returned error, not have hidden from them by a "successful" retry. + // 2. If every retry also failed, `resp` was never reassigned from its + // original nil value and there was no `return` for this case, so + // control fell through to `resp.StatusCode` below on a nil + // *http.Response, panicking the caller (e.g. crashing `terraform + // apply` outright). + // Callers that need retry-with-backoff semantics around a timeout (and + // that know their request is safe to repeat) should implement that at + // their own call site, where they have the context to decide; this layer + // now always returns the transport error untouched. switch { - case respErr != nil && (strings.Contains(respErr.Error(), "context deadline exceeded")): - sleepDuration := time.Duration(1) * time.Second - for i := 0; i < MAX_CONTEXT_DEADLINE_RETRIES; i++ { - // sleep for exponential backoff - if i > 0 { - sleepDuration *= 2 - if sleepDuration > time.Duration(MAX_WAIT_SECONDS)*time.Second { - sleepDuration = time.Duration(MAX_WAIT_SECONDS) * time.Second - } - log.Printf( - "[DEBUG] %s request to %s failed with error %s, retrying in %s seconds...", - request.Method, - keyfactorPath, - respErr.Error(), - sleepDuration, - ) - time.Sleep(sleepDuration) - } - - log.Printf( - "[DEBUG] %s request to %s failed with error %s, retrying...", - request.Method, - keyfactorPath, - respErr.Error(), - ) - req, reqErr = http.NewRequest(request.Method, keyfactorPath, bytes.NewBuffer(jsonByes)) - if reqErr != nil { - return nil, reqErr - } - resp2, respErr2 := httpClient.Do(req) - if respErr2 == nil && resp2 != nil { - resp = resp2 - break - } - } case respErr != nil: log.Printf("[ERROR] Error sending '%s' request to '%s': %s", request.Method, request.Endpoint, respErr) return nil, respErr diff --git a/v3/api/client_test.go b/v3/api/client_test.go index f256d40..cf8d4a0 100644 --- a/v3/api/client_test.go +++ b/v3/api/client_test.go @@ -23,6 +23,7 @@ import ( "net/http/httptest" "net/url" "os" + "strings" "sync" "sync/atomic" "testing" @@ -449,3 +450,141 @@ func TestClient_ConcurrentRequestsNotCappedByMaxConnsPerHost(t *testing.T) { ) } } + +// TestSendRequest_ContextDeadlineExceeded_NoSilentRetry reproduces the first half +// of a confirmed HIGH-severity bug in sendRequest: on a client-side timeout +// ("context deadline exceeded"), the function used to transparently retry the +// request (up to MAX_CONTEXT_DEADLINE_RETRIES times) and, if a retry +// succeeded, return that success with no indication a timeout ever happened. +// +// That silently swallowed the fact that the *original* request may have +// already succeeded server-side (e.g. a certificate enrollment), and +// defeated callers -- like terraform-provider-keyfactor's orphaned-PFX +// recovery logic -- that specifically match on "context deadline exceeded" +// to trigger their own safe recovery search instead of blindly re-issuing a +// non-idempotent request. +// +// This test sets up a server that only delays its FIRST response beyond the +// client timeout and answers instantly after that -- i.e. exactly the shape +// that used to be masked into a silent "success after retry". It asserts +// sendRequest now returns the timeout error immediately, without contacting +// the server a second time. +func TestSendRequest_ContextDeadlineExceeded_NoSilentRetry(t *testing.T) { + var requestCount int32 + + srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + n := atomic.AddInt32(&requestCount, 1) + if n == 1 { + // First request: sleep well past the client timeout below, so the + // client gives up on it -- but note the server continues to + // process it and will "complete" it right after. A retry that + // followed would succeed immediately, which is exactly the + // scenario that used to be silently swallowed. + time.Sleep(300 * time.Millisecond) + } + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(`{}`)) + })) + defer srv.Close() + + c := &Client{ + AuthClient: &mockAuthConfig{ + serverConfig: newTestServerConfig(srv), + httpClient: testHTTPClientWithTimeout(srv, 50*time.Millisecond), + }, + } + + resp, err := c.sendRequest(&request{Method: http.MethodGet, Endpoint: "test", Headers: &apiHeaders{}}) + + if err == nil { + t.Fatalf("sendRequest() returned nil error, want a context-deadline-exceeded error (retry must not silently succeed)") + } + if !strings.Contains(err.Error(), "context deadline exceeded") { + t.Fatalf("sendRequest() error = %q, want it to contain %q", err.Error(), "context deadline exceeded") + } + if resp != nil { + t.Fatalf("sendRequest() returned a non-nil response alongside an error: %+v", resp) + } + if got := atomic.LoadInt32(&requestCount); got != 1 { + t.Fatalf( + "server was hit %d time(s), want exactly 1 -- sendRequest must not silently retry a timed-out request", + got, + ) + } +} + +// TestSendRequest_ContextDeadlineExceeded_NoPanic reproduces the second half +// of the bug: when every attempt fails with a "context deadline exceeded" +// shaped error, sendRequest's response variable was never reassigned from +// its original nil value, and the surrounding switch had no `return` for +// this case -- so control fell through to `resp.StatusCode` on a nil +// *http.Response, panicking the caller (e.g. crashing `terraform apply` +// outright) instead of returning a normal, handleable error. +// +// This test uses a server that always delays past the client timeout, so +// every attempt sendRequest makes hits the same failure shape. It asserts a +// clean error is returned -- never a panic. +func TestSendRequest_ContextDeadlineExceeded_NoPanic(t *testing.T) { + srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + time.Sleep(300 * time.Millisecond) + w.WriteHeader(http.StatusOK) + })) + defer srv.Close() + + c := &Client{ + AuthClient: &mockAuthConfig{ + serverConfig: newTestServerConfig(srv), + httpClient: testHTTPClientWithTimeout(srv, 50*time.Millisecond), + }, + } + + var ( + resp *http.Response + err error + ) + + func() { + defer func() { + if r := recover(); r != nil { + t.Fatalf("sendRequest() panicked: %v", r) + } + }() + resp, err = c.sendRequest(&request{Method: http.MethodGet, Endpoint: "test", Headers: &apiHeaders{}}) + }() + + if err == nil { + t.Fatalf("sendRequest() returned nil error, want a context-deadline-exceeded error") + } + if !strings.Contains(err.Error(), "context deadline exceeded") { + t.Fatalf("sendRequest() error = %q, want it to contain %q", err.Error(), "context deadline exceeded") + } + if resp != nil { + t.Fatalf("sendRequest() returned a non-nil response alongside an error: %+v", resp) + } +} + +// newTestServerConfig builds a minimal *auth_providers.Server pointing at the +// given httptest server, matching the pattern used by newTestClient in +// pam_types_test.go. +func newTestServerConfig(server *httptest.Server) *auth_providers.Server { + return &auth_providers.Server{ + Host: server.URL, + APIPath: "/KeyfactorAPI", + SkipTLSVerify: true, + } +} + +// testHTTPClientWithTimeout returns an *http.Client that trusts the given +// httptest TLS server's certificate (sendRequest always forces the https +// scheme, so a plain httptest.NewServer can't be used directly) but with a +// short overall Timeout so requests to a deliberately slow handler produce a +// real "context deadline exceeded" error, identical in shape to what a +// production client sees against a genuinely slow/unreachable Command +// server. +func testHTTPClientWithTimeout(server *httptest.Server, timeout time.Duration) *http.Client { + base := server.Client() + return &http.Client{ + Transport: base.Transport, + Timeout: timeout, + } +} From 9f66f38c4df8009bd982108d10ea3713648aa0ac Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 26 Aug 2026 09:35:17 -0700 Subject: [PATCH 50/51] fix(client): redact secrets before logging via Go's global log package This library redirects Go's global log package to tflog via a TerraformLogger whose ctx is captured once at client-construction time (initLogger), so per-call masking applied by callers to this library's own tflog calls can never reach these log.Printf/log.Println call sites. Three call sites confirmed dumping secret material in plaintext: - client.go's sendRequest logged the full JSON-marshaled request body at TRACE level for every API call (enrollment, store, PAM payloads, etc.), and separately, logRequest (invoked on every request) dumped the same body a further three times: as a JSON blob, as a directly-replayable cURL command, and as a base64-encoded cURL command - the cURL forms are a more severe leak than a plain log dump since they're copy-paste runnable by anyone who reads the log. - EnrollPFXV2 logged its enrollment args (including the PFX password) at TRACE level. In the current code this specific log.Println call happens not to print the password in cleartext, because its argument is wrapped in an extra, incidental pointer level (Payload: &ea, where ea is already *EnrollPFXFctArgsV2) that changes fmt's default formatting to a hex address instead of the dereferenced struct - but relying on that as protection is fragile and not an intentional safeguard; a well-meaning cleanup of that stray "&" would silently reintroduce a real leak here. Fixed explicitly regardless. - RecoverCertificate logged its args (including the private-key recovery Password) at DEBUG level - a routine troubleshooting verbosity, not one requiring unusual verbosity to trigger, and reachable on ordinary Read/Update/import private-key-recovery paths. This one demonstrably did leak the plaintext password via fmt's default struct formatting. Adds a shared redactSensitiveJSONForLogging helper (log_redaction.go) that recursively redacts JSON object keys matching a case-insensitive password/secret/token/private-key pattern, used for the generic request-body log sites in client.go, plus explicit redacted-copy logging at the two certificate.go call sites. None of this touches the actual bytes sent as the outgoing HTTP request body - only what gets logged. Adds regression tests (log_redaction_test.go) that capture Go's global log output and assert a canary password never appears, for both RecoverCertificate (DEBUG, confirmed leaking pre-fix) and EnrollPFXV2 (TRACE; the real leak in the pre-fix code flows through the generic request-body log in client.go rather than EnrollPFXV2's own log.Println, for the pointer-wrapping reason above - the test still passes post-fix and protects both paths going forward). --- v3/api/certificate.go | 29 +++++++- v3/api/client.go | 18 +++-- v3/api/log_redaction.go | 90 +++++++++++++++++++++++++ v3/api/log_redaction_test.go | 124 +++++++++++++++++++++++++++++++++++ 4 files changed, 254 insertions(+), 7 deletions(-) create mode 100644 v3/api/log_redaction.go create mode 100644 v3/api/log_redaction_test.go diff --git a/v3/api/certificate.go b/v3/api/certificate.go index 700083e..c4edf1f 100644 --- a/v3/api/certificate.go +++ b/v3/api/certificate.go @@ -142,7 +142,21 @@ func (c *Client) EnrollPFXV2(ea *EnrollPFXFctArgsV2) (*EnrollResponseV2, error) Payload: &ea, } - log.Println("[TRACE] Request: ", keyfactorAPIStruct) + // Log a redacted copy of the enrollment args rather than ea/keyfactorAPIStruct + // directly: ea.Password carries the PFX private-key protection password, and + // %v-formatting the struct (as this TRACE log historically did) would dump it + // in plaintext. redactedEA is a value copy (ea is *EnrollPFXFctArgsV2) so + // mutating its Password field below never touches the real request's ea. + redactedEA := *ea + if redactedEA.Password != "" { + redactedEA.Password = redactedLogValue + } + log.Println("[TRACE] Request: ", &request{ + Method: keyfactorAPIStruct.Method, + Endpoint: keyfactorAPIStruct.Endpoint, + Headers: keyfactorAPIStruct.Headers, + Payload: &redactedEA, + }) resp, err := c.sendRequest(keyfactorAPIStruct) if err != nil { @@ -725,7 +739,18 @@ func (c *Client) RecoverCertificate( IncludeChain: true, } - log.Println("[DEBUG] RecoverCertificate: Recovering certificate with args:", rca) + // Log a redacted copy: rca.Password is the private-key recovery password + // supplied by the caller, and this DEBUG-level log (a common + // troubleshooting verbosity, reachable on ordinary Read/Update/import + // private-key-recovery paths) used to dump it in plaintext via %v-style + // struct formatting. redactedRCA is a value copy (rca is + // *recoverCertArgs) so mutating its Password field below never touches + // the real rca used to build the outgoing request below. + redactedRCA := *rca + if redactedRCA.Password != "" { + redactedRCA.Password = redactedLogValue + } + log.Println("[DEBUG] RecoverCertificate: Recovering certificate with args:", &redactedRCA) // Set Keyfactor-specific headers headers := &apiHeaders{ Headers: []StringTuple{ diff --git a/v3/api/client.go b/v3/api/client.go index c1b0eca..4320056 100644 --- a/v3/api/client.go +++ b/v3/api/client.go @@ -249,7 +249,12 @@ func logRequest(req *http.Request) error { // Restore the request body so it can be read later req.Body = io.NopCloser(bytes.NewBuffer(body)) - // Create a struct to hold request data + // Create a struct to hold request data. The body is redacted before + // logging (see redactSensitiveJSONForLogging) since it's always the same + // JSON-marshaled request.Payload passed into sendRequest, which may carry + // a certificate/PFX recovery password or other secret - this must not be + // dumped verbatim into TRACE-level logs. + redactedBody := redactSensitiveJSONForLogging(body) requestData := struct { Method string `json:"method"` URL string `json:"url"` @@ -259,7 +264,7 @@ func logRequest(req *http.Request) error { Method: req.Method, URL: req.URL.String(), Headers: req.Header, - Body: string(body), + Body: string(redactedBody), } // Convert struct to JSON @@ -296,7 +301,10 @@ func requestToCurl(req *http.Request) (string, error) { } } - // Add the body if it exists + // Add the body if it exists. The body is redacted before being embedded + // in the logged cURL command (see redactSensitiveJSONForLogging) since a + // TRACE-level cURL command containing a raw password is directly + // replayable by anyone who reads the log, not just informational. if req.Method == http.MethodPost || req.Method == http.MethodPut { body, err := io.ReadAll(req.Body) if err != nil { @@ -304,7 +312,7 @@ func requestToCurl(req *http.Request) (string, error) { } req.Body = io.NopCloser(bytes.NewBuffer(body)) // Restore the request body - curlCommand.WriteString(fmt.Sprintf("--data %q ", string(body))) + curlCommand.WriteString(fmt.Sprintf("--data %q ", string(redactSensitiveJSONForLogging(body)))) } return curlCommand.String(), nil @@ -362,7 +370,7 @@ func (c *Client) sendRequest(request *request) (*http.Response, error) { if mErr != nil { return nil, mErr } - log.Printf("[TRACE] Request body: %s", jsonByes) + log.Printf("[TRACE] Request body: %s", redactSensitiveJSONForLogging(jsonByes)) req, reqErr := http.NewRequest(request.Method, keyfactorPath, bytes.NewBuffer(jsonByes)) if reqErr != nil { diff --git a/v3/api/log_redaction.go b/v3/api/log_redaction.go new file mode 100644 index 0000000..9831d01 --- /dev/null +++ b/v3/api/log_redaction.go @@ -0,0 +1,90 @@ +// Copyright 2026 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "encoding/json" + "regexp" +) + +// sensitiveLogFieldPattern matches JSON object keys that are known or likely +// to carry secret material: certificate/PFX passwords, PAM secret values, +// private key material, API tokens, etc. It is intentionally broad (a +// case-insensitive substring match, not an exact-name allowlist) because this +// package's request payloads are logged generically at TRACE/DEBUG level +// without per-call-site awareness of which fields are sensitive - erring +// toward redacting a borderline non-sensitive field (e.g. "PasswordOptions", +// a policy-description object, not a secret) in a debug log is a far cheaper +// mistake than missing a real secret. +var sensitiveLogFieldPattern = regexp.MustCompile(`(?i)(password|passphrase|secret|privatekey|private_key|pfx|apikey|api_key|accesstoken|access_token|clientsecret|client_secret|token)`) + +const redactedLogValue = "[REDACTED]" + +// redactSensitiveJSONForLogging returns a copy of jsonBytes (expected to be +// the JSON encoding of an API request/response payload) with the values of +// any object key matching sensitiveLogFieldPattern replaced by +// redactedLogValue, so that logging the payload for troubleshooting doesn't +// also leak certificate/PFX recovery passwords or other secrets into the +// application log (which historically was set to Go's global *log* package, +// bypassing any per-call caller-side masking applied to this library's own +// tflog calls). +// +// This function is used purely to sanitize what gets written to a log line; +// it never mutates or replaces the original bytes used to build the actual +// outgoing HTTP request body. +// +// If jsonBytes doesn't decode as JSON (e.g. it's the literal "null", or +// malformed), it is returned unchanged: there is no structured content to +// redact, and logging a bare scalar isn't a plausible secret-leak vector on +// its own. +func redactSensitiveJSONForLogging(jsonBytes []byte) []byte { + var decoded interface{} + if err := json.Unmarshal(jsonBytes, &decoded); err != nil { + return jsonBytes + } + + redacted, err := json.Marshal(redactSensitiveValue(decoded)) + if err != nil { + return jsonBytes + } + return redacted +} + +// redactSensitiveValue recursively walks a decoded JSON value (as produced by +// encoding/json's default interface{} unmarshaling: map[string]interface{}, +// []interface{}, or a scalar), replacing the value of any map key that +// matches sensitiveLogFieldPattern with redactedLogValue. +func redactSensitiveValue(v interface{}) interface{} { + switch t := v.(type) { + case map[string]interface{}: + out := make(map[string]interface{}, len(t)) + for k, val := range t { + if sensitiveLogFieldPattern.MatchString(k) { + out[k] = redactedLogValue + } else { + out[k] = redactSensitiveValue(val) + } + } + return out + case []interface{}: + out := make([]interface{}, len(t)) + for i, val := range t { + out[i] = redactSensitiveValue(val) + } + return out + default: + return v + } +} diff --git a/v3/api/log_redaction_test.go b/v3/api/log_redaction_test.go new file mode 100644 index 0000000..90e8cb5 --- /dev/null +++ b/v3/api/log_redaction_test.go @@ -0,0 +1,124 @@ +// Copyright 2026 Keyfactor +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package api + +import ( + "bytes" + "log" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +// canaryRecoveryPassword is a value distinctive enough that its presence in +// captured log output can only be explained by the password itself leaking, +// not by an unrelated log line coincidentally matching. +const canaryRecoveryPassword = "Sup3rS3cr3t-Canary-Password-DoNotLeak" + +// captureGlobalLogOutput redirects Go's global *log* package output (the +// same package this library's client.go/certificate.go call sites use, and +// the same package initLogger redirects to a TerraformLogger/tflog in +// production) to an in-memory buffer for the duration of the test, restoring +// whatever writer was previously configured afterward. This lets tests +// assert on the exact text that would have been logged without needing a +// live tflog/terraform-plugin-log sink. +func captureGlobalLogOutput(t *testing.T) *bytes.Buffer { + t.Helper() + var buf bytes.Buffer + original := log.Writer() + log.SetOutput(&buf) + t.Cleanup(func() { + log.SetOutput(original) + }) + return &buf +} + +// TestRecoverCertificate_DoesNotLogPlaintextPassword is a regression test for +// a confirmed leak: RecoverCertificate's args (including the private-key +// recovery Password) used to be dumped verbatim via +// `log.Println("[DEBUG] RecoverCertificate: Recovering certificate with +// args:", rca)` -- reachable on ordinary Read/Update/import private-key +// recovery paths at DEBUG level, a routine troubleshooting verbosity, not +// something requiring an unusually verbose log level to trigger. +// +// This drives the real RecoverCertificate function (not just the log +// statement in isolation) against a minimal mock server, so it reproduces +// exactly what a caller doing certificate recovery observes in their logs. +// The server intentionally returns a non-2xx status so RecoverCertificate +// exits with an error shortly after the log statement under test, without +// requiring a valid PFX/PKCS12 response body to be fabricated. +func TestRecoverCertificate_DoesNotLogPlaintextPassword(t *testing.T) { + srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(`{"Message":"synthetic failure for test"}`)) + })) + defer srv.Close() + + c := newTestClient(srv) + + buf := captureGlobalLogOutput(t) + + // Return values are intentionally ignored: the server above always + // errors, so RecoverCertificate is expected to return a non-nil error. + // What matters for this test is only what got written to the log before + // that error was returned. + _, _, _, _, _ = c.RecoverCertificate(123, "", "", "", canaryRecoveryPassword, 0, "PFX") + + logged := buf.String() + if strings.Contains(logged, canaryRecoveryPassword) { + t.Fatalf( + "RecoverCertificate logged the plaintext recovery password via Go's global log package; captured log output:\n%s", + logged, + ) + } +} + +// TestEnrollPFXV2_DoesNotLogPlaintextPassword is a regression test for a +// confirmed leak: EnrollPFXV2's request struct (whose Payload carries +// ea.Password, the PFX private-key protection password) used to be dumped +// verbatim via `log.Println("[TRACE] Request: ", keyfactorAPIStruct)`. +// +// This drives the real EnrollPFXV2 function against a minimal mock server +// that always errors, so the function returns shortly after the log +// statement under test without requiring a valid enrollment response body. +func TestEnrollPFXV2_DoesNotLogPlaintextPassword(t *testing.T) { + srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(`{"Message":"synthetic failure for test"}`)) + })) + defer srv.Close() + + c := newTestClient(srv) + + buf := captureGlobalLogOutput(t) + + _, _ = c.EnrollPFXV2(&EnrollPFXFctArgsV2{ + Template: "test-template", + CertFormat: "PFX", + SubjectString: "CN=test.example.com", + Password: canaryRecoveryPassword, + }) + + logged := buf.String() + if strings.Contains(logged, canaryRecoveryPassword) { + t.Fatalf( + "EnrollPFXV2 logged the plaintext PFX password via Go's global log package; captured log output:\n%s", + logged, + ) + } +} From e9612c8fb5aeaeb560c7b02de5f7b35a71ea7e25 Mon Sep 17 00:00:00 2001 From: spbsoluble <1661003+spbsoluble@users.noreply.github.com> Date: Wed, 26 Aug 2026 10:20:24 -0700 Subject: [PATCH 51/51] fix(client): redact secrets nested inside pre-serialized JSON string fields redactSensitiveValue treated any JSON string leaf as opaque, so request structs that pre-serialize a map to a JSON string before the outer struct is marshaled again (e.g. CreateStoreFctArgs/UpdateStoreFctArgs's PropertiesString field, populated with ServerUsername/ServerPassword on every certificate-store create/update) leaked those secrets in plaintext into [TRACE] request-body/cURL logs, unaffected by the redaction added in 9f66f38. String leaves are now given one extra chance: if they successfully decode as JSON to a map or array, that decoded structure is redacted recursively and re-marshaled back to a string, preserving the "JSON encoded as a string" shape in the log output. A capped recursion depth (5) prevents adversarial or accidental deep string-of-JSON-of-string nesting from recursing unboundedly. --- v3/api/log_redaction.go | 70 ++++++++++++++++-- v3/api/log_redaction_test.go | 138 +++++++++++++++++++++++++++++++++++ 2 files changed, 202 insertions(+), 6 deletions(-) diff --git a/v3/api/log_redaction.go b/v3/api/log_redaction.go index 9831d01..315f615 100644 --- a/v3/api/log_redaction.go +++ b/v3/api/log_redaction.go @@ -32,6 +32,20 @@ var sensitiveLogFieldPattern = regexp.MustCompile(`(?i)(password|passphrase|secr const redactedLogValue = "[REDACTED]" +// maxNestedJSONStringDepth bounds how many levels of "JSON encoded as a +// string value" redactSensitiveValue will attempt to unmarshal and recurse +// into. Several request structs (e.g. CreateStoreFctArgs/UpdateStoreFctArgs's +// PropertiesString field) are populated by pre-serializing a +// map[string]interface{} to a JSON string before the outer struct itself is +// marshaled by sendRequest, producing a JSON string leaf whose *contents* are +// themselves JSON containing secrets (e.g. ServerUsername/ServerPassword). +// Without unwrapping these string-encoded-JSON leaves, redaction never sees +// the nested keys and secrets sail through unredacted into TRACE logs. The +// depth guard exists purely so adversarial or accidentally-deep +// string-of-JSON-of-string-of-JSON... nesting can't recurse unboundedly; a +// legitimate payload should never come close to this limit. +const maxNestedJSONStringDepth = 5 + // redactSensitiveJSONForLogging returns a copy of jsonBytes (expected to be // the JSON encoding of an API request/response payload) with the values of // any object key matching sensitiveLogFieldPattern replaced by @@ -55,7 +69,7 @@ func redactSensitiveJSONForLogging(jsonBytes []byte) []byte { return jsonBytes } - redacted, err := json.Marshal(redactSensitiveValue(decoded)) + redacted, err := json.Marshal(redactSensitiveValue(decoded, maxNestedJSONStringDepth)) if err != nil { return jsonBytes } @@ -64,9 +78,29 @@ func redactSensitiveJSONForLogging(jsonBytes []byte) []byte { // redactSensitiveValue recursively walks a decoded JSON value (as produced by // encoding/json's default interface{} unmarshaling: map[string]interface{}, -// []interface{}, or a scalar), replacing the value of any map key that -// matches sensitiveLogFieldPattern with redactedLogValue. -func redactSensitiveValue(v interface{}) interface{} { +// []interface{}, a string, or another scalar), replacing the value of any +// map key that matches sensitiveLogFieldPattern with redactedLogValue. +// +// String leaves get one extra check: some request structs pre-serialize a +// map to a JSON string before the outer struct is marshaled again (e.g. +// CreateStoreFctArgs/UpdateStoreFctArgs's PropertiesString field), so a +// string leaf's own contents may themselves be JSON carrying secrets +// (ServerUsername/ServerPassword, etc.) that would otherwise sail through +// unredacted. If a string leaf successfully unmarshals as a +// map[string]interface{} or []interface{}, it is redacted the same way and +// re-marshaled back to a string, preserving its "valid JSON encoded as a +// string" shape in the log output. If it doesn't decode to one of those two +// container types (including "it isn't valid JSON at all"), it is left +// alone: that's the common case of an ordinary string value, not a nested +// payload to unwrap. +// +// remainingDepth bounds how many further levels of string-encoded-JSON will +// be unwrapped, so adversarial or accidentally deep nesting +// (JSON-of-string-of-JSON-of-string-of-...) can't recurse unboundedly. Once +// it reaches zero, string leaves are left as-is without attempting to parse +// them further; map/slice recursion is unaffected by this guard since it can +// only nest as deeply as the decoded value's own structure allows. +func redactSensitiveValue(v interface{}, remainingDepth int) interface{} { switch t := v.(type) { case map[string]interface{}: out := make(map[string]interface{}, len(t)) @@ -74,16 +108,40 @@ func redactSensitiveValue(v interface{}) interface{} { if sensitiveLogFieldPattern.MatchString(k) { out[k] = redactedLogValue } else { - out[k] = redactSensitiveValue(val) + out[k] = redactSensitiveValue(val, remainingDepth) } } return out case []interface{}: out := make([]interface{}, len(t)) for i, val := range t { - out[i] = redactSensitiveValue(val) + out[i] = redactSensitiveValue(val, remainingDepth) } return out + case string: + if remainingDepth <= 0 { + return t + } + var nested interface{} + if err := json.Unmarshal([]byte(t), &nested); err != nil { + return t + } + switch nested.(type) { + case map[string]interface{}, []interface{}: + redactedNested := redactSensitiveValue(nested, remainingDepth-1) + reMarshaled, err := json.Marshal(redactedNested) + if err != nil { + return t + } + return string(reMarshaled) + default: + // Decoded to a bare scalar (a JSON string/number/bool/null that + // happens to be valid JSON on its own, e.g. the string "42" or + // `"true"`) rather than a container - nothing to redact inside + // it, and re-marshaling would just be a no-op wrapped in + // pointless work. Leave the original string leaf unchanged. + return t + } default: return v } diff --git a/v3/api/log_redaction_test.go b/v3/api/log_redaction_test.go index 90e8cb5..de89e69 100644 --- a/v3/api/log_redaction_test.go +++ b/v3/api/log_redaction_test.go @@ -16,6 +16,7 @@ package api import ( "bytes" + "encoding/json" "log" "net/http" "net/http/httptest" @@ -87,6 +88,143 @@ func TestRecoverCertificate_DoesNotLogPlaintextPassword(t *testing.T) { } } +// TestCreateStore_DoesNotLogPlaintextServerPasswordInNestedProperties is a +// regression test for a confirmed leak that is distinct from the +// top-level-field leaks covered above: CreateStoreFctArgs (and +// UpdateStoreFctArgs) carry certificate store connection properties as +// PropertiesString, a JSON string PRE-SERIALIZED from the Properties map by +// CreateStore/UpdateStore themselves (see store.go) before the outer struct +// is marshaled again by sendRequest. That produces a JSON *string* value at +// the top level of the outer request object whose own contents are +// themselves JSON containing secrets such as ServerUsername/ServerPassword - +// exactly the shape real callers hit today (confirmed usage: +// terraform-provider-keyfactor's certificate store resource populates +// ServerPassword/ServerUsername in this Properties map on every create and +// update call). redactSensitiveValue's original string case treated any +// string leaf as opaque, so this nested JSON-as-a-string secret sailed +// through unredacted into the [TRACE] request-body log even though the +// top-level "Password" field redacted fine. +// +// This drives the real CreateStore function (not just the redaction helper +// in isolation) against a minimal mock server, so it reproduces exactly what +// a caller creating a certificate store observes in their logs. +func TestCreateStore_DoesNotLogPlaintextServerPasswordInNestedProperties(t *testing.T) { + srv := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(`{"Message":"synthetic failure for test"}`)) + })) + defer srv.Close() + + c := newTestClient(srv) + + buf := captureGlobalLogOutput(t) + + // Return value is intentionally ignored: the server above always errors, + // so CreateStore is expected to return a non-nil error. What matters for + // this test is only what got written to the log before that error was + // returned. + _, _ = c.CreateStore(&CreateStoreFctArgs{ + ClientMachine: "test-client-machine", + StorePath: "/opt/test/store", + AgentId: "11111111-1111-1111-1111-111111111111", + Properties: map[string]interface{}{ + "ServerUsername": "admin", + "ServerPassword": canaryRecoveryPassword, + }, + }) + + logged := buf.String() + if strings.Contains(logged, canaryRecoveryPassword) { + t.Fatalf( + "CreateStore logged the plaintext ServerPassword nested inside the pre-serialized Properties JSON string; captured log output:\n%s", + logged, + ) + } +} + +// TestRedactSensitiveValue_UnwrapsNestedJSONEncodedAsString is a narrower +// unit test against redactSensitiveValue directly (rather than a full +// end-to-end CreateStore drive), pinning down the exact contract: a string +// leaf whose own contents decode to a JSON object or array gets redacted the +// same way a "real" nested object would, and the result is re-marshaled back +// to a string so the log output keeps the "valid JSON encoded as a string" +// shape instead of silently becoming a nested object. +func TestRedactSensitiveValue_UnwrapsNestedJSONEncodedAsString(t *testing.T) { + outer := map[string]interface{}{ + "ClientMachine": "test-client-machine", + "Properties": `{"ServerUsername":"admin","ServerPassword":"` + canaryRecoveryPassword + `"}`, + } + + redacted := redactSensitiveValue(outer, maxNestedJSONStringDepth) + + redactedBytes, err := json.Marshal(redacted) + if err != nil { + t.Fatalf("failed to marshal redacted value: %v", err) + } + if strings.Contains(string(redactedBytes), canaryRecoveryPassword) { + t.Fatalf("redactSensitiveValue did not redact a secret nested inside a JSON-encoded-as-string leaf; got: %s", redactedBytes) + } + + // The Properties value should still be a JSON string (not have been + // promoted to a nested object), and it should still be valid JSON once + // unwrapped, with ServerUsername left intact and only ServerPassword + // redacted. + redactedMap, ok := redacted.(map[string]interface{}) + if !ok { + t.Fatalf("expected top-level redacted value to remain a map, got %T", redacted) + } + propertiesVal, ok := redactedMap["Properties"].(string) + if !ok { + t.Fatalf("expected Properties to remain a JSON-encoded string, got %T: %v", redactedMap["Properties"], redactedMap["Properties"]) + } + var reparsed map[string]interface{} + if err := json.Unmarshal([]byte(propertiesVal), &reparsed); err != nil { + t.Fatalf("Properties string is no longer valid JSON after redaction: %v", err) + } + if reparsed["ServerUsername"] != "admin" { + t.Fatalf("expected non-sensitive ServerUsername to survive redaction unchanged, got: %v", reparsed["ServerUsername"]) + } + if reparsed["ServerPassword"] != redactedLogValue { + t.Fatalf("expected ServerPassword to be redacted to %q, got: %v", redactedLogValue, reparsed["ServerPassword"]) + } +} + +// TestRedactSensitiveValue_DepthGuardStopsOnDeeplyNestedJSONStrings ensures +// the recursion depth guard actually bounds how many levels of +// string-encoded-JSON get unwrapped, so adversarial or accidental +// JSON-of-string-of-JSON-of-string-of-... nesting can't cause unbounded +// recursion. It doesn't assert a specific leak/no-leak outcome beyond the +// guard depth (that's an explicit, documented tradeoff) - only that +// redaction terminates and produces valid JSON. +func TestRedactSensitiveValue_DepthGuardStopsOnDeeplyNestedJSONStrings(t *testing.T) { + // Build a value nested well beyond maxNestedJSONStringDepth: each layer + // is a JSON object whose single field's value is itself a JSON-encoded + // string of the next layer down, innermost carrying the canary secret. + value := canaryRecoveryPassword + for i := 0; i < maxNestedJSONStringDepth+5; i++ { + layer, err := json.Marshal(map[string]interface{}{"Secret": value}) + if err != nil { + t.Fatalf("failed to build nested fixture at layer %d: %v", i, err) + } + value = string(layer) + } + + var decoded interface{} + if err := json.Unmarshal([]byte(value), &decoded); err != nil { + t.Fatalf("failed to decode fixture: %v", err) + } + + redacted := redactSensitiveValue(decoded, maxNestedJSONStringDepth) + + // This must not hang or panic (the real assertion here is that the test + // completes at all); as a secondary check, confirm the result still + // marshals to valid JSON. + if _, err := json.Marshal(redacted); err != nil { + t.Fatalf("redacted deeply-nested value failed to re-marshal: %v", err) + } +} + // TestEnrollPFXV2_DoesNotLogPlaintextPassword is a regression test for a // confirmed leak: EnrollPFXV2's request struct (whose Payload carries // ea.Password, the PFX private-key protection password) used to be dumped