diff --git a/go.mod b/go.mod index de93d95f..cffc76dd 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,8 @@ require ( aead.dev/mem v0.2.0 aead.dev/minisign v0.2.1 cloud.google.com/go/secretmanager v1.11.4 + github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0 + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.0.1 github.com/Azure/go-autorest/autorest v0.11.29 github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 github.com/aws/aws-sdk-go v1.49.17 @@ -31,12 +33,16 @@ require ( cloud.google.com/go/compute v1.23.3 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect cloud.google.com/go/iam v1.1.5 // indirect + github.com/Azure/azure-sdk-for-go/sdk/azcore v1.8.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 // indirect + github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 // indirect github.com/Azure/go-autorest v14.2.0+incompatible // indirect github.com/Azure/go-autorest/autorest/adal v0.9.23 // indirect github.com/Azure/go-autorest/autorest/azure/cli v0.4.6 // indirect github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect github.com/Azure/go-autorest/logger v0.2.1 // indirect github.com/Azure/go-autorest/tracing v0.6.0 // indirect + github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cenkalti/backoff/v3 v3.2.2 // indirect @@ -47,9 +53,11 @@ require ( github.com/go-logr/logr v1.4.1 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/golang-jwt/jwt/v4 v4.5.0 // indirect + github.com/golang-jwt/jwt/v5 v5.0.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/s2a-go v0.1.7 // indirect + github.com/google/uuid v1.5.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect github.com/googleapis/gax-go/v2 v2.12.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect @@ -63,6 +71,7 @@ require ( github.com/hashicorp/hcl v1.0.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/kr/text v0.2.0 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.20 // indirect @@ -72,6 +81,7 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/muesli/reflow v0.3.0 // indirect github.com/philhofer/fwd v1.1.2 // indirect + github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/rivo/uniseg v0.4.4 // indirect diff --git a/go.sum b/go.sum index f872c5d8..af2f9de2 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,16 @@ cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI= cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= cloud.google.com/go/secretmanager v1.11.4 h1:krnX9qpG2kR2fJ+u+uNyNo+ACVhplIAS4Pu7u+4gd+k= cloud.google.com/go/secretmanager v1.11.4/go.mod h1:wreJlbS9Zdq21lMzWmJ0XhWW2ZxgPeahsqeV/vZoJ3w= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.8.0 h1:9kDVnTz3vbfweTqAUmk/a/pH5pWFCHtvRpHYC0G/dcA= +github.com/Azure/azure-sdk-for-go/sdk/azcore v1.8.0/go.mod h1:3Ug6Qzto9anB6mGlEdgYMDF5zHQ+wwhEaYR4s17PHMw= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0 h1:BMAjVKJM0U/CYF27gA0ZMmXGkOcvfFtD0oHVZ1TIPRI= +github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0/go.mod h1:1fXstnBMas5kzG+S3q8UoJcmyU6nUeunJcMDHcRYHhs= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0 h1:sXr+ck84g/ZlZUOZiNELInmMgOsuGwdjjVkEIde0OtY= +github.com/Azure/azure-sdk-for-go/sdk/internal v1.3.0/go.mod h1:okt5dMMTOFjX/aovMlrjvvXoPMBVSPzk9185BT0+eZM= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.0.1 h1:8TkzQBrN9PWIwo7ekdd696KpC6IfTltV2/F8qKKBWik= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets v1.0.1/go.mod h1:aprFpXPQiTyG5Rkz6Ot5pvU6y6YKg/AKYOcLCoxN0bk= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0 h1:D3occbWoio4EBLkbkevetNMAVX197GkzbUMtqjGWn80= +github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v1.0.0/go.mod h1:bTSOgj05NGRuHHhQwAdPnYr9TOdNmKlZTgGLL6nyAdI= github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= github.com/Azure/go-autorest/autorest v0.11.24/go.mod h1:G6kyRlFnTuSbEYkQGawPfsCswgme4iYf6rfSKUDzbCc= @@ -37,6 +47,8 @@ github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+Z github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= +github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1 h1:WpB/QDNLpMw72xHJc34BNNykqSOeEJDAWkhf0u12/Jk= +github.com/AzureAD/microsoft-authentication-library-for-go v1.1.1/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/aws/aws-sdk-go v1.49.17 h1:Cc+7LgPjKeJkF2SdNo1IkpQ5Dfl9HCZEVw9OP3CPuEI= github.com/aws/aws-sdk-go v1.49.17/go.mod h1:LF8svs817+Nz+DmiMQKTO3ubZ/6IaTpq3TjupRn3Eqk= @@ -61,6 +73,8 @@ 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/dimchansky/utfbom v1.1.1 h1:vV6w1AhK4VMnhBno/TPVCoK9U/LP0PkLCS9tbxHdi/U= github.com/dimchansky/utfbom v1.1.1/go.mod h1:SxdoEBH5qIqFocHMyGOXVAybYJdr71b1Q/j0mACtrfE= +github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= +github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -84,6 +98,8 @@ github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzw github.com/golang-jwt/jwt/v4 v4.2.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang-jwt/jwt/v5 v5.0.0 h1:1n1XNM9hk7O9mnQoNBGolZvzebBQ7p93ULHRc28XJUE= +github.com/golang-jwt/jwt/v5 v5.0.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= @@ -114,6 +130,8 @@ github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeN github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= +github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= @@ -150,6 +168,8 @@ 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= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -176,6 +196,8 @@ github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8= github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= +github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= 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/prometheus/client_golang v1.18.0 h1:HzFfmkOzH5Q8L8G+kSJKUx5dtG87sewO+FoDDqP5Tbk= @@ -272,6 +294,7 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210228012217-479acdf4ea46/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/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-20210616045830-e2b7044e8c71/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.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/keystore/azure/client.go b/internal/keystore/azure/client.go index de445ef9..91fde516 100644 --- a/internal/keystore/azure/client.go +++ b/internal/keystore/azure/client.go @@ -8,6 +8,7 @@ import ( "bytes" "context" "encoding/json" + "errors" "fmt" "math" "net/http" @@ -16,6 +17,7 @@ import ( "time" "aead.dev/mem" + "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets" "github.com/Azure/go-autorest/autorest" xhttp "github.com/minio/kes/internal/http" "github.com/minio/kes/internal/key" @@ -33,9 +35,10 @@ type status struct { } type client struct { - Endpoint string - Authorizer autorest.Authorizer - Client xhttp.Retry + Endpoint string + Authorizer autorest.Authorizer + Client xhttp.Retry + azsecretsClient *azsecrets.Client } // CreateSecret creates a KeyVault secret with @@ -48,6 +51,26 @@ type client struct { // then KeyVault will not return an error but create // another version of the secret with the given value. func (c *client) CreateSecret(ctx context.Context, name, value string) (status, error) { + if c.azsecretsClient != nil { + _, err := c.azsecretsClient.SetSecret(ctx, name, azsecrets.SetSecretParameters{ + Value: &value, + }, &azsecrets.SetSecretOptions{}) + if err != nil { + azResp, ok := TransportErrToResponseError(err) + if !ok { + return status{}, err + } + return status{ + StatusCode: azResp.StatusCode, + ErrorCode: azResp.ErrorCode, + Message: azResp.errorResponse.Error.Message, + }, nil + } + return status{ + StatusCode: http.StatusOK, + }, nil + } + type Request struct { Value string `json:"value"` } @@ -108,6 +131,54 @@ func (c *client) CreateSecret(ctx context.Context, name, value string) (status, // if the secret is disabled, expired or should not // be used, yet. func (c *client) GetSecret(ctx context.Context, name, version string) (string, status, error) { + if c.azsecretsClient != nil { + response, err := c.azsecretsClient.GetSecret(ctx, name, "", &azsecrets.GetSecretOptions{}) + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + return "", status{}, err + } + if err != nil { + azResp, ok := TransportErrToResponseError(err) + if !ok { + return "", status{}, err + } + return "", status{ + StatusCode: azResp.StatusCode, + ErrorCode: azResp.ErrorCode, + Message: azResp.errorResponse.Error.Message, + }, nil + } + if response.Attributes.Enabled != nil && !*response.Attributes.Enabled { + return "", status{ + StatusCode: http.StatusUnprocessableEntity, + ErrorCode: "ObjectIsDisabled", + Message: fmt.Sprintf("The secret %q is current disabled and cannot be used", name), + }, nil + } + if response.Attributes.NotBefore != nil && time.Since(*response.Attributes.NotBefore) <= 0 { + return "", status{ + StatusCode: http.StatusUnprocessableEntity, + ErrorCode: "ObjectMustNotBeUsed", + Message: fmt.Sprintf("The secret %q must not be used before %v", name, *response.Attributes.NotBefore), + }, nil + } + if response.Attributes.Expires != nil && time.Until(*response.Attributes.Expires) <= 0 { + return "", status{ + StatusCode: http.StatusUnprocessableEntity, + ErrorCode: "ObjectIsExpired", + Message: fmt.Sprintf("The secret %q is expired and cannot be used", name), + }, nil + } + + if response.Value != nil { + return *response.Value, status{ + StatusCode: http.StatusOK, + }, nil + } + return "", status{ + StatusCode: http.StatusOK, + }, nil + } + type Response struct { Value string `json:"value"` Attr struct { @@ -194,6 +265,23 @@ func (c *client) GetSecret(ctx context.Context, name, version string) (string, s // even if it returns 200 OK. Instead, the secret may be in // a transition state from "active" to (soft) deleted. func (c *client) DeleteSecret(ctx context.Context, name string) (status, error) { + if c.azsecretsClient != nil { + _, err := c.azsecretsClient.DeleteSecret(ctx, name, &azsecrets.DeleteSecretOptions{}) + if err != nil { + azResp, ok := TransportErrToResponseError(err) + if !ok { + return status{}, err + } + return status{ + StatusCode: azResp.StatusCode, + ErrorCode: azResp.ErrorCode, + Message: azResp.errorResponse.Error.Message, + }, nil + } + return status{ + StatusCode: http.StatusOK, + }, nil + } uri := endpoint(c.Endpoint, "secrets", name) + "?api-version=7.2" req, err := http.NewRequestWithContext(ctx, http.MethodDelete, uri, nil) if err != nil { @@ -232,6 +320,24 @@ func (c *client) DeleteSecret(ctx context.Context, name string) (status, error) // recovered. Therefore, deleting a KeyVault secret permanently is // a two-step process. func (c *client) PurgeSecret(ctx context.Context, name string) (status, error) { + if c.azsecretsClient != nil { + _, err := c.azsecretsClient.PurgeDeletedSecret(ctx, name, &azsecrets.PurgeDeletedSecretOptions{}) + if err != nil { + azResp, ok := TransportErrToResponseError(err) + if !ok { + return status{}, err + } + return status{ + StatusCode: azResp.StatusCode, + ErrorCode: azResp.ErrorCode, + Message: azResp.errorResponse.Error.Message, + }, nil + } + return status{ + StatusCode: http.StatusOK, + }, nil + } + uri := endpoint(c.Endpoint, "deletedsecrets", name) + "?api-version=7.2" req, err := http.NewRequestWithContext(ctx, http.MethodDelete, uri, nil) if err != nil { @@ -270,6 +376,53 @@ func (c *client) PurgeSecret(ctx context.Context, name string) (status, error) { // versions of the given secret. When a secret contains more then 25 // versions GetFirstVersions returns a status with a 422 HTTP error code. func (c *client) GetFirstVersion(ctx context.Context, name string) (string, status, error) { + if c.azsecretsClient != nil { + pager := c.azsecretsClient.NewListSecretPropertiesVersionsPager(name, &azsecrets.ListSecretPropertiesVersionsOptions{}) + if pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + azResp, ok := TransportErrToResponseError(err) + if !ok { + return "", status{}, err + } + return "", status{ + StatusCode: azResp.StatusCode, + ErrorCode: azResp.ErrorCode, + Message: azResp.errorResponse.Error.Message, + }, nil + } + if page.SecretPropertiesListResult.NextLink != nil && *page.SecretPropertiesListResult.NextLink != "" { + return "", status{ + StatusCode: http.StatusUnprocessableEntity, + ErrorCode: "TooManyObjectVersions", + Message: fmt.Sprintf("There are too many versions of %q.", name), + }, nil + } + if len(page.SecretPropertiesListResult.Value) == 0 { + return "", status{ + StatusCode: http.StatusNotFound, + ErrorCode: "NoObjectVersions", + Message: fmt.Sprintf("There are no versions of %q.", name), + }, nil + } + var ( + id string // most recent Secret ID + createdAt int64 = math.MaxInt64 // most recent createdAt UNIX timestamp + ) + for _, v := range page.SecretPropertiesListResult.Value { + if v.Attributes != nil && v.Attributes.Created != nil && v.ID != nil { + if createdAt > (*v.Attributes.Created).Unix() { + createdAt = (*v.Attributes.Created).Unix() + id = v.ID.Version() + } + } + } + return path.Base(id), status{ + StatusCode: http.StatusOK, + }, nil + } + } + type Response struct { Versions []struct { ID string `json:"id"` diff --git a/internal/keystore/azure/key-vault-error.go b/internal/keystore/azure/key-vault-error.go new file mode 100644 index 00000000..9b43ecd8 --- /dev/null +++ b/internal/keystore/azure/key-vault-error.go @@ -0,0 +1,41 @@ +package azure + +import ( + "net/http" + "reflect" +) + +// ResponseError is a wrapper around an error response from the Azure Key Vault service. +type ResponseError struct { + // ErrorCode is the error code returned by the resource provider if available. + ErrorCode string + + // StatusCode is the HTTP status code as defined in https://pkg.go.dev/net/http#pkg-constants. + StatusCode int + + // RawResponse is the underlying HTTP response. + RawResponse *http.Response + + errorResponse errorResponse +} + +// TransportErrToResponseError converts a transport error to a ResponseError. +func TransportErrToResponseError(terr error) (*ResponseError, bool) { + if reflect.TypeOf(terr).String() == "*exported.ResponseError" { + tv := reflect.ValueOf(terr).Elem() + ErrorCode := tv.FieldByName("ErrorCode").String() + StatusCode := int(tv.FieldByName("StatusCode").Int()) + RawResponse, ok := tv.FieldByName("RawResponse").Interface().(*http.Response) + var errorResponse errorResponse + if ok { + errorResponse, _ = parseErrorResponse(RawResponse) + } + return &ResponseError{ + ErrorCode: ErrorCode, + StatusCode: StatusCode, + RawResponse: RawResponse, + errorResponse: errorResponse, + }, true + } + return nil, false +} diff --git a/internal/keystore/azure/key-vault.go b/internal/keystore/azure/key-vault.go index a3ba7024..d20f146a 100644 --- a/internal/keystore/azure/key-vault.go +++ b/internal/keystore/azure/key-vault.go @@ -10,8 +10,13 @@ import ( "fmt" "math/rand" "net/http" + "os" "time" + "github.com/Azure/azure-sdk-for-go/sdk/azcore" + "github.com/Azure/azure-sdk-for-go/sdk/azcore/policy" + "github.com/Azure/azure-sdk-for-go/sdk/azidentity" + "github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azsecrets" "github.com/Azure/go-autorest/autorest" "github.com/Azure/go-autorest/autorest/azure/auth" "github.com/minio/kes" @@ -307,6 +312,32 @@ func (s *Store) Get(ctx context.Context, name string) ([]byte, error) { // or when there are no (more) keys starting with the prefix, the // returned prefix is empty func (s *Store) List(ctx context.Context, prefix string, n int) ([]string, string, error) { + if s.client.azsecretsClient != nil { + var names []string + pager := s.client.azsecretsClient.NewListSecretPropertiesPager(&azsecrets.ListSecretPropertiesOptions{}) + for pager.More() { + page, err := pager.NextPage(ctx) + if err != nil { + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + return nil, "", err + } + azResp, ok := TransportErrToResponseError(err) + if !ok { + return nil, "", err + } + return nil, "", fmt.Errorf("azure: failed to list keys: %s (%s)", azResp.ErrorCode, azResp.errorResponse.Error.Message) + } + for _, v := range page.SecretPropertiesListResult.Value { + if v.ID != nil { + names = append(names, (*v.ID).Name()) + } + } + if page.NextLink == nil || *page.NextLink == "" { + break + } + } + return keystore.List(names, prefix, n) + } var ( names []string nextLink string @@ -338,6 +369,33 @@ func (s *Store) Close() error { return nil } // ConnectWithCredentials tries to establish a connection to a Azure KeyVault // instance using Azure client credentials. func ConnectWithCredentials(_ context.Context, endpoint string, creds Credentials) (*Store, error) { + if os.Getenv("AZURE_CLIENT_API_VERSION") != "7.2" { + os.Setenv("AZURE_CLIENT_ID", creds.ClientID) + os.Setenv("AZURE_CLIENT_SECRET", creds.Secret) + os.Setenv("AZURE_TENANT_ID", creds.TenantID) + cred, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + return nil, fmt.Errorf("azure: failed to create default Azure credential: %v", err) + } + azsecretsClient, err := azsecrets.NewClient(endpoint, cred, &azsecrets.ClientOptions{ + ClientOptions: azcore.ClientOptions{ + Retry: policy.RetryOptions{ + MaxRetries: 7, + RetryDelay: 200 * time.Millisecond, + MaxRetryDelay: 800 * time.Millisecond, + }, + }, + }) + if err != nil { + return nil, fmt.Errorf("azure: failed to create secrets client: %v", err) + } + return &Store{ + endpoint: endpoint, + client: client{ + azsecretsClient: azsecretsClient, + }, + }, nil + } const Scope = "https://vault.azure.net" c := auth.NewClientCredentialsConfig(creds.ClientID, creds.Secret, creds.TenantID) @@ -358,6 +416,31 @@ func ConnectWithCredentials(_ context.Context, endpoint string, creds Credential // ConnectWithIdentity tries to establish a connection to a Azure KeyVault // instance using an Azure managed identity. func ConnectWithIdentity(_ context.Context, endpoint string, msi ManagedIdentity) (*Store, error) { + if os.Getenv("AZURE_CLIENT_API_VERSION") != "7.2" { + os.Setenv("AZURE_CLIENT_ID", msi.ClientID) + cred, err := azidentity.NewDefaultAzureCredential(nil) + if err != nil { + return nil, fmt.Errorf("azure: failed to create default Azure credential: %v", err) + } + azsecretsClient, err := azsecrets.NewClient(endpoint, cred, &azsecrets.ClientOptions{ + ClientOptions: azcore.ClientOptions{ + Retry: policy.RetryOptions{ + MaxRetries: 7, + RetryDelay: 200 * time.Millisecond, + MaxRetryDelay: 800 * time.Millisecond, + }, + }, + }) + if err != nil { + return nil, fmt.Errorf("azure: failed to create secrets client: %v", err) + } + return &Store{ + endpoint: endpoint, + client: client{ + azsecretsClient: azsecretsClient, + }, + }, nil + } const Scope = "https://vault.azure.net" c := auth.NewMSIConfig() diff --git a/internal/keystore/azure/key-vault_test.go b/internal/keystore/azure/key-vault_test.go new file mode 100644 index 00000000..33981f8a --- /dev/null +++ b/internal/keystore/azure/key-vault_test.go @@ -0,0 +1,79 @@ +package azure + +import ( + "context" + "os" + "testing" +) + +func TestConnectWithCredentials(t *testing.T) { + ClientID := os.Getenv("ClientID") + TenantID := os.Getenv("TenantID") + Secret := os.Getenv("Secret") + EndPoint := os.Getenv("EndPoint") + if ClientID == "" || TenantID == "" || Secret == "" || EndPoint == "" { + t.Skip("Skipping test due to missing credentials") + } + ctx := context.Background() + os.Setenv("AZURE_CLIENT_API_VERSION", "7.2") + c1, err := ConnectWithCredentials(ctx, EndPoint, Credentials{TenantID: TenantID, ClientID: ClientID, Secret: Secret}) + if err != nil { + return + } + os.Setenv("AZURE_CLIENT_API_VERSION", "7.4") + c2, err := ConnectWithCredentials(ctx, EndPoint, Credentials{TenantID: TenantID, ClientID: ClientID, Secret: Secret}) + if err != nil { + return + } + c3, err := ConnectWithIdentity(ctx, EndPoint, ManagedIdentity{ClientID: ClientID}) + if err != nil { + return + } + { + // delete first + _ = c1.Delete(ctx, "mytestFirst-c1") + // create + err = c1.Create(ctx, "mytestFirst-c1", []byte("hello")) + if err != nil { + t.Error(err) + } + data, err := c1.Get(ctx, "mytestFirst-c1") + t.Logf("data:[%s] err:[%v]\n", data, err) + + list, s, err := c1.List(ctx, "", 25) + t.Logf("list:[%s] s:[%s] err:[%v]\n", list, s, err) + t.Log("-------------------------------") + } + { + // delete first + _ = c2.Delete(ctx, "mytestFirst-c2") + // create + err = c2.Create(ctx, "mytestFirst-c2", []byte("hello")) + if err != nil { + t.Error(err) + } + data, err := c2.Get(ctx, "mytestFirst-c2") + t.Logf("data:[%s] err:[%v]\n", data, err) + + list, s, err := c2.List(ctx, "", 25) + t.Logf("list:[%s] s:[%s] err:[%v]\n", list, s, err) + t.Log("-------------------------------") + } + { + // delete first + _ = c3.Delete(ctx, "mytestFirst-c3") + // create + err = c3.Create(ctx, "mytestFirst-c3", []byte("hello")) + if err != nil { + t.Error(err) + } + data, err := c3.Get(ctx, "mytestFirst-c3") + t.Logf("data:[%s] err:[%v]\n", data, err) + list, s, err := c3.List(ctx, "", 25) + t.Logf("list:[%s] s:[%s] err:[%v]\n", list, s, err) + t.Log("-------------------------------") + } + _ = c1 + _ = c2 + _ = c3 +}