diff --git a/README.md b/README.md index 10b46bc2a..ab2378327 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,7 @@ Multiple people and organizations are joining efforts to create a single Externa - [AWS Secrets Manager](https://external-secrets.io/provider-aws-secrets-manager/) - [AWS Parameter Store](https://external-secrets.io/provider-aws-parameter-store/) +- [Akeyless](https://www.akeyless.io/) - [Hashicorp Vault](https://www.vaultproject.io/) - [Google Cloud Secrets Manager](https://external-secrets.io/provider-google-secrets-manager/) - [Azure Key Vault](https://external-secrets.io/provider-azure-key-vault/) @@ -20,7 +21,7 @@ Multiple people and organizations are joining efforts to create a single Externa - [Yandex Lockbox](https://external-secrets.io/provider-yandex-lockbox/) - [Gitlab Project Variables](https://external-secrets.io/provider-gitlab-project-variables/) - [Alibaba Cloud KMS](https://www.alibabacloud.com/product/kms) (Docs still missing, PRs welcomed!) -- [Oracle Vault]( https://external-secrets.io/provider-oracle-vault) +- [Oracle Vault]( https://external-secrets.io/provider-oracle-vault) ## Stability and Support Level @@ -43,6 +44,7 @@ Multiple people and organizations are joining efforts to create a single Externa | [Gitlab Project Variables](https://external-secrets.io/provider-gitlab-project-variables/) | alpha | [@Jabray5](https://github.com/Jabray5) | | Alibaba Cloud KMS | alpha | [@ElsaChelala](https://github.com/ElsaChelala) | | [Oracle Vault]( https://external-secrets.io/provider-oracle-vault) | alpha | [@KianTigger](https://github.com/KianTigger) | +| [Akeyless]( https://external-secrets.io/provider-akeyless) | alpha | [@renanaAkeyless](https://github.com/renanaAkeyless) | ## Documentation diff --git a/apis/externalsecrets/v1alpha1/secretstore_akeyless_types.go b/apis/externalsecrets/v1alpha1/secretstore_akeyless_types.go new file mode 100644 index 000000000..14ab4e708 --- /dev/null +++ b/apis/externalsecrets/v1alpha1/secretstore_akeyless_types.go @@ -0,0 +1,42 @@ +/* +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 v1alpha1 + +import ( + esmeta "github.com/external-secrets/external-secrets/apis/meta/v1" +) + +// AkeylessProvider Configures an store to sync secrets using Akeyless KV. +type AkeylessProvider struct { + + // Akeyless GW API Url from which the secrets to be fetched from. + AkeylessGWApiURL *string `json:"akeylessGWApiURL"` + + // Auth configures how the operator authenticates with Akeyless. + Auth *AkeylessAuth `json:"authSecretRef"` +} + +type AkeylessAuth struct { + SecretRef AkeylessAuthSecretRef `json:"secretRef"` +} + +// AkeylessAuthSecretRef +//AKEYLESS_ACCESS_TYPE_PARAM: AZURE_OBJ_ID OR GCP_AUDIENCE OR ACCESS_KEY OR KUB_CONFIG_NAME. +type AkeylessAuthSecretRef struct { + // The SecretAccessID is used for authentication + AccessID esmeta.SecretKeySelector `json:"accessID,omitempty"` + AccessType esmeta.SecretKeySelector `json:"accessType,omitempty"` + AccessTypeParam esmeta.SecretKeySelector `json:"accessTypeParam,omitempty"` +} diff --git a/apis/externalsecrets/v1alpha1/secretstore_types.go b/apis/externalsecrets/v1alpha1/secretstore_types.go index adf136dae..eb44faeab 100644 --- a/apis/externalsecrets/v1alpha1/secretstore_types.go +++ b/apis/externalsecrets/v1alpha1/secretstore_types.go @@ -42,6 +42,10 @@ type SecretStoreProvider struct { // +optional AzureKV *AzureKVProvider `json:"azurekv,omitempty"` + // Akeyless configures this store to sync secrets using Akeyless Vault provider + // +optional + Akeyless *AkeylessProvider `json:"akeyless,omitempty"` + // Vault configures this store to sync secrets using Hashi provider // +optional Vault *VaultProvider `json:"vault,omitempty"` diff --git a/apis/externalsecrets/v1alpha1/zz_generated.deepcopy.go b/apis/externalsecrets/v1alpha1/zz_generated.deepcopy.go index 08268c277..bb550dec2 100644 --- a/apis/externalsecrets/v1alpha1/zz_generated.deepcopy.go +++ b/apis/externalsecrets/v1alpha1/zz_generated.deepcopy.go @@ -102,6 +102,65 @@ func (in *AWSProvider) DeepCopy() *AWSProvider { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AkeylessAuth) DeepCopyInto(out *AkeylessAuth) { + *out = *in + in.SecretRef.DeepCopyInto(&out.SecretRef) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AkeylessAuth. +func (in *AkeylessAuth) DeepCopy() *AkeylessAuth { + if in == nil { + return nil + } + out := new(AkeylessAuth) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AkeylessAuthSecretRef) DeepCopyInto(out *AkeylessAuthSecretRef) { + *out = *in + in.AccessID.DeepCopyInto(&out.AccessID) + in.AccessType.DeepCopyInto(&out.AccessType) + in.AccessTypeParam.DeepCopyInto(&out.AccessTypeParam) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AkeylessAuthSecretRef. +func (in *AkeylessAuthSecretRef) DeepCopy() *AkeylessAuthSecretRef { + if in == nil { + return nil + } + out := new(AkeylessAuthSecretRef) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AkeylessProvider) DeepCopyInto(out *AkeylessProvider) { + *out = *in + if in.AkeylessGWApiURL != nil { + in, out := &in.AkeylessGWApiURL, &out.AkeylessGWApiURL + *out = new(string) + **out = **in + } + if in.Auth != nil { + in, out := &in.Auth, &out.Auth + *out = new(AkeylessAuth) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AkeylessProvider. +func (in *AkeylessProvider) DeepCopy() *AkeylessProvider { + if in == nil { + return nil + } + out := new(AkeylessProvider) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AlibabaAuth) DeepCopyInto(out *AlibabaAuth) { *out = *in @@ -794,6 +853,11 @@ func (in *SecretStoreProvider) DeepCopyInto(out *SecretStoreProvider) { *out = new(AzureKVProvider) (*in).DeepCopyInto(*out) } + if in.Akeyless != nil { + in, out := &in.Akeyless, &out.Akeyless + *out = new(AkeylessProvider) + (*in).DeepCopyInto(*out) + } if in.Vault != nil { in, out := &in.Vault, &out.Vault *out = new(VaultProvider) diff --git a/deploy/crds/external-secrets.io_clustersecretstores.yaml b/deploy/crds/external-secrets.io_clustersecretstores.yaml index 0db2425ab..02649746c 100644 --- a/deploy/crds/external-secrets.io_clustersecretstores.yaml +++ b/deploy/crds/external-secrets.io_clustersecretstores.yaml @@ -54,6 +54,94 @@ spec: maxProperties: 1 minProperties: 1 properties: + akeyless: + description: Akeyless configures this store to sync secrets using + Akeyless Vault provider + properties: + akeylessGWApiURL: + description: Akeyless GW API Url from which the secrets to + be fetched from. + type: string + authSecretRef: + description: Auth configures how the operator authenticates + with Akeyless. + properties: + secretRef: + description: 'AkeylessAuthSecretRef AKEYLESS_ACCESS_TYPE_PARAM: + AZURE_OBJ_ID OR GCP_AUDIENCE OR ACCESS_KEY OR KUB_CONFIG_NAME.' + properties: + accessID: + description: The SecretAccessID is used for authentication + properties: + key: + description: The key of the entry in the Secret + resource's `data` field to be used. Some instances + of this field may be defaulted, in others it + may be required. + type: string + name: + description: The name of the Secret resource being + referred to. + type: string + namespace: + description: Namespace of the resource being referred + to. Ignored if referent is not cluster-scoped. + cluster-scoped defaults to the namespace of + the referent. + type: string + type: object + accessType: + description: A reference to a specific 'key' within + a Secret resource, In some instances, `key` is a + required field. + properties: + key: + description: The key of the entry in the Secret + resource's `data` field to be used. Some instances + of this field may be defaulted, in others it + may be required. + type: string + name: + description: The name of the Secret resource being + referred to. + type: string + namespace: + description: Namespace of the resource being referred + to. Ignored if referent is not cluster-scoped. + cluster-scoped defaults to the namespace of + the referent. + type: string + type: object + accessTypeParam: + description: A reference to a specific 'key' within + a Secret resource, In some instances, `key` is a + required field. + properties: + key: + description: The key of the entry in the Secret + resource's `data` field to be used. Some instances + of this field may be defaulted, in others it + may be required. + type: string + name: + description: The name of the Secret resource being + referred to. + type: string + namespace: + description: Namespace of the resource being referred + to. Ignored if referent is not cluster-scoped. + cluster-scoped defaults to the namespace of + the referent. + type: string + type: object + type: object + required: + - secretRef + type: object + required: + - akeylessGWApiURL + - authSecretRef + type: object alibaba: description: Alibaba configures this store to sync secrets using Alibaba Cloud provider diff --git a/deploy/crds/external-secrets.io_secretstores.yaml b/deploy/crds/external-secrets.io_secretstores.yaml index e5152167d..b6c6b65ad 100644 --- a/deploy/crds/external-secrets.io_secretstores.yaml +++ b/deploy/crds/external-secrets.io_secretstores.yaml @@ -54,6 +54,94 @@ spec: maxProperties: 1 minProperties: 1 properties: + akeyless: + description: Akeyless configures this store to sync secrets using + Akeyless Vault provider + properties: + akeylessGWApiURL: + description: Akeyless GW API Url from which the secrets to + be fetched from. + type: string + authSecretRef: + description: Auth configures how the operator authenticates + with Akeyless. + properties: + secretRef: + description: 'AkeylessAuthSecretRef AKEYLESS_ACCESS_TYPE_PARAM: + AZURE_OBJ_ID OR GCP_AUDIENCE OR ACCESS_KEY OR KUB_CONFIG_NAME.' + properties: + accessID: + description: The SecretAccessID is used for authentication + properties: + key: + description: The key of the entry in the Secret + resource's `data` field to be used. Some instances + of this field may be defaulted, in others it + may be required. + type: string + name: + description: The name of the Secret resource being + referred to. + type: string + namespace: + description: Namespace of the resource being referred + to. Ignored if referent is not cluster-scoped. + cluster-scoped defaults to the namespace of + the referent. + type: string + type: object + accessType: + description: A reference to a specific 'key' within + a Secret resource, In some instances, `key` is a + required field. + properties: + key: + description: The key of the entry in the Secret + resource's `data` field to be used. Some instances + of this field may be defaulted, in others it + may be required. + type: string + name: + description: The name of the Secret resource being + referred to. + type: string + namespace: + description: Namespace of the resource being referred + to. Ignored if referent is not cluster-scoped. + cluster-scoped defaults to the namespace of + the referent. + type: string + type: object + accessTypeParam: + description: A reference to a specific 'key' within + a Secret resource, In some instances, `key` is a + required field. + properties: + key: + description: The key of the entry in the Secret + resource's `data` field to be used. Some instances + of this field may be defaulted, in others it + may be required. + type: string + name: + description: The name of the Secret resource being + referred to. + type: string + namespace: + description: Namespace of the resource being referred + to. Ignored if referent is not cluster-scoped. + cluster-scoped defaults to the namespace of + the referent. + type: string + type: object + type: object + required: + - secretRef + type: object + required: + - akeylessGWApiURL + - authSecretRef + type: object alibaba: description: Alibaba configures this store to sync secrets using Alibaba Cloud provider diff --git a/docs/provider-akeyless.md b/docs/provider-akeyless.md new file mode 100644 index 000000000..1843868dd --- /dev/null +++ b/docs/provider-akeyless.md @@ -0,0 +1,68 @@ +## Akeyless Vault + +External Secrets Operator integrates with [Akeyless API](https://docs.akeyless.io/reference#v2). + +### Authentication + +The API requires an access-id, access-type and access-Type-param. + +The supported auth-methods and their params are: + +| accessType | accessTypeParam | +| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `api_key` | The access key. | +| `k8s` | The k8s configuration name | +| `aws_iam` | - | +| `gcp` | The gcp audience | +| `azure_ad` | azure object id (optional) | + +form more information about [Akeyless Authentication Methods](https://docs.akeyless.io/docs/access-and-authentication-methods) + +### Akeless credentials secret + +Create a secret containing your credentials: + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: akeylss-secret-creds +type: Opaque +stringData: + accessId: "p-XXXX" + accessType: # k8s/aws_iam/gcp/azure_ad/api_key + accessTypeParam: # can be one of the following: k8s-conf-name/gcp-audience/azure-obj-id/access-key +``` + +### Update secret store +Be sure the `akeyless` provider is listed in the `Kind=SecretStore` and the `akeylessGWApiURL` is set (def: "https://api.akeless.io". + +```yaml +{% include 'akeyless-secret-store.yaml' %} +``` + +### Creating external secret + +To get a secret from Akeyless and secret it on the Kubernetes cluster, a `Kind=ExternalSecret` is needed. + +```yaml +{% include 'akeyless-external-secret.yaml' %} +``` + +#### Using DataFrom + +DataFrom can be used to get a secret as a JSON string and attempt to parse it. + +```yaml +{% include 'akeyless-external-secret-json.yaml' %} +``` + +### Getting the Kubernetes secret +The operator will fetch the secret and inject it as a `Kind=Secret`. +``` +kubectl get secret akeyless-secret-to-create -o jsonpath='{.data.secretKey}' | base64 -d +``` + +``` +kubectl get secret akeyless-secret-to-create-json -o jsonpath='{.data}' +``` diff --git a/docs/snippets/akeyless-credentials-secret.yaml b/docs/snippets/akeyless-credentials-secret.yaml new file mode 100644 index 000000000..d31cf8243 --- /dev/null +++ b/docs/snippets/akeyless-credentials-secret.yaml @@ -0,0 +1,12 @@ +apiVersion: v1 +kind: Secret +metadata: + name: akeylss-secret-creds +type: Opaque +stringData: + accessId: "p-XXXX" + accessType: # k8s/aws_iam/gcp/azure_ad/api_key + accessTypeParam: # can be one of the following: k8s-conf-name/gcp-audience/azure-obj-id/access-key + + + diff --git a/docs/snippets/akeyless-external-secret-json.yaml b/docs/snippets/akeyless-external-secret-json.yaml new file mode 100644 index 000000000..20744db62 --- /dev/null +++ b/docs/snippets/akeyless-external-secret-json.yaml @@ -0,0 +1,18 @@ +apiVersion: external-secrets.io/v1alpha1 +kind: ExternalSecret +metadata: + name: akeyless-external-secret-example-json +spec: + refreshInterval: 1h + + secretStoreRef: + kind: SecretStore + name: akeyless-secret-store # Must match SecretStore on the cluster + + target: + name: akeyless-secret-to-create-json # Name for the secret to be created on the cluster + creationPolicy: Owner + + # for json formatted secrets: each key in the json will be used as the secret key in the SECRET k8s target object + dataFrom: + - key: secret-name # Full path of the secret on Akeyless diff --git a/docs/snippets/akeyless-external-secret.yaml b/docs/snippets/akeyless-external-secret.yaml new file mode 100644 index 000000000..27eed4ba5 --- /dev/null +++ b/docs/snippets/akeyless-external-secret.yaml @@ -0,0 +1,19 @@ +apiVersion: external-secrets.io/v1alpha1 +kind: ExternalSecret +metadata: + name: akeyless-external-secret-example +spec: + refreshInterval: 1h + + secretStoreRef: + kind: SecretStore + name: akeyless-secret-store # Must match SecretStore on the cluster + + target: + name: akeyless-secret-to-create # Name for the secret to be created on the cluster + creationPolicy: Owner + + data: + - secretKey: secretKey # Key given to the secret to be created on the cluster + remoteRef: + key: secret-name # Full path of the secret on Akeyless diff --git a/docs/snippets/akeyless-secret-store.yaml b/docs/snippets/akeyless-secret-store.yaml new file mode 100644 index 000000000..b41b2643c --- /dev/null +++ b/docs/snippets/akeyless-secret-store.yaml @@ -0,0 +1,20 @@ +apiVersion: external-secrets.io/v1alpha1 +kind: SecretStore +metadata: + name: akeyless-secret-store +spec: + provider: + akeyless: + # URL of your akeyless API + akeylessGWApiURL: "https://api.akeyless.io" + authSecretRef: + secretRef: + accessID: + name: akeylss-secret-creds + key: accessId + accessType: + name: akeylss-secret-creds + key: accessType + accessTypeParam: + name: akeylss-secret-creds + key: accessTypeParam diff --git a/e2e/run.sh b/e2e/run.sh index 1b8718007..55edfcd7e 100755 --- a/e2e/run.sh +++ b/e2e/run.sh @@ -54,6 +54,9 @@ kubectl run --rm \ --env="GCP_PROJECT_ID=${GCP_PROJECT_ID:-}" \ --env="AZURE_CLIENT_ID=${AZURE_CLIENT_ID:-}" \ --env="AZURE_CLIENT_SECRET=${AZURE_CLIENT_SECRET:-}" \ + --env="AKEYLESS_ACCESS_ID=${AKEYLESS_ACCESS_ID:-}" \ + --env="AKEYLESS_ACCESS_TYPE=${AKEYLESS_ACCESS_TYPE:-}" \ + --env="AKEYLESS_ACCESS_TYPE_PARAM=${AKEYLESS_ACCESS_TYPE_PARAM:-}" \ --env="TENANT_ID=${TENANT_ID:-}" \ --env="VAULT_URL=${VAULT_URL:-}" \ --env="GITLAB_TOKEN=${GITLAB_TOKEN:-}" \ diff --git a/e2e/suite/akeyless/akeyless.go b/e2e/suite/akeyless/akeyless.go new file mode 100644 index 000000000..de8ad93b6 --- /dev/null +++ b/e2e/suite/akeyless/akeyless.go @@ -0,0 +1,49 @@ +/* +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 akeyless + +import ( + "os" + + // nolint + . "github.com/onsi/ginkgo" + // nolint + . "github.com/onsi/ginkgo/extensions/table" + + "github.com/external-secrets/external-secrets/e2e/framework" + "github.com/external-secrets/external-secrets/e2e/suite/common" +) + +var _ = Describe("[akeyless] ", func() { + f := framework.New("eso-akeyless") + accessID := os.Getenv("AKEYLESS_ACCESS_ID") + accessType := os.Getenv("AKEYLESS_ACCESS_TYPE") + accessTypeParam := os.Getenv("AKEYLESS_ACCESS_TYPE_PARAM") + prov := newAkeylessProvider(f, accessID, accessType, accessTypeParam) + + DescribeTable("sync secrets", framework.TableFunc(f, prov), + Entry(common.SimpleDataSync(f)), + Entry(common.NestedJSONWithGJSON(f)), + Entry(common.JSONDataFromSync(f)), + Entry(common.JSONDataWithProperty(f)), + Entry(common.JSONDataWithTemplate(f)), + Entry(common.DockerJSONConfig(f)), + Entry(common.DataPropertyDockerconfigJSON(f)), + Entry(common.SSHKeySync(f)), + Entry(common.SSHKeySyncDataProperty(f)), + Entry(common.SyncWithoutTargetName(f)), + Entry(common.JSONDataWithoutTargetName(f)), + ) +}) diff --git a/e2e/suite/akeyless/provider.go b/e2e/suite/akeyless/provider.go new file mode 100644 index 000000000..8528c41db --- /dev/null +++ b/e2e/suite/akeyless/provider.go @@ -0,0 +1,227 @@ +/* +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 akeyless + +import ( + "context" + "encoding/base64" + "errors" + "fmt" + "io/ioutil" + "os" + "strings" + + aws_cloud_id "github.com/akeylesslabs/akeyless-go-cloud-id/cloudprovider/aws" + azure_cloud_id "github.com/akeylesslabs/akeyless-go-cloud-id/cloudprovider/azure" + gcp_cloud_id "github.com/akeylesslabs/akeyless-go-cloud-id/cloudprovider/gcp" + "github.com/akeylesslabs/akeyless-go/v2" + + //nolint + . "github.com/onsi/ginkgo" + + //nolint + . "github.com/onsi/gomega" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1" + esmeta "github.com/external-secrets/external-secrets/apis/meta/v1" + "github.com/external-secrets/external-secrets/e2e/framework" +) + +type akeylessProvider struct { + accessID string + accessType string + accessTypeParam string + framework *framework.Framework + restAPIClient *akeyless.V2ApiService +} + +var apiErr akeyless.GenericOpenAPIError + +const DefServiceAccountFile = "/var/run/secrets/kubernetes.io/serviceaccount/token" + +func newAkeylessProvider(f *framework.Framework, accessID, accessType, accessTypeParam string) *akeylessProvider { + prov := &akeylessProvider{ + accessID: accessID, + accessType: accessType, + accessTypeParam: accessTypeParam, + framework: f, + } + + restAPIClient := akeyless.NewAPIClient(&akeyless.Configuration{ + Servers: []akeyless.ServerConfiguration{ + { + URL: "https://api.akeyless.io", + }, + }, + }).V2Api + + prov.restAPIClient = restAPIClient + + BeforeEach(prov.BeforeEach) + return prov +} + +// CreateSecret creates a secret. +func (a *akeylessProvider) CreateSecret(key, val string) { + token, err := a.GetToken() + Expect(err).ToNot(HaveOccurred()) + + ctx := context.Background() + gsvBody := akeyless.CreateSecret{ + Name: key, + Value: val, + Token: &token, + } + + _, _, err = a.restAPIClient.CreateSecret(ctx).Body(gsvBody).Execute() + Expect(err).ToNot(HaveOccurred()) +} + +func (a *akeylessProvider) DeleteSecret(key string) { + token, err := a.GetToken() + Expect(err).ToNot(HaveOccurred()) + + ctx := context.Background() + gsvBody := akeyless.DeleteItem{ + Name: key, + Token: &token, + } + + _, _, err = a.restAPIClient.DeleteItem(ctx).Body(gsvBody).Execute() + Expect(err).ToNot(HaveOccurred()) +} + +func (a *akeylessProvider) BeforeEach() { + // Creating an Akeyless secret + akeylessCreds := &v1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: "provider-secret", + Namespace: a.framework.Namespace.Name, + }, + StringData: map[string]string{ + "access-id": a.accessID, + "access-type": a.accessType, + "access-type-param": a.accessTypeParam, + }, + } + err := a.framework.CRClient.Create(context.Background(), akeylessCreds) + Expect(err).ToNot(HaveOccurred()) + + // Creating Akeyless secret store + secretStore := &esv1alpha1.SecretStore{ + ObjectMeta: metav1.ObjectMeta{ + Name: a.framework.Namespace.Name, + Namespace: a.framework.Namespace.Name, + }, + Spec: esv1alpha1.SecretStoreSpec{ + Provider: &esv1alpha1.SecretStoreProvider{ + Akeyless: &esv1alpha1.AkeylessProvider{ + Auth: &esv1alpha1.AkeylessAuth{ + SecretRef: esv1alpha1.AkeylessAuthSecretRef{ + AccessID: esmeta.SecretKeySelector{ + Name: "access-id-secret", + Key: "access-id", + }, + AccessType: esmeta.SecretKeySelector{ + Name: "access-type-secret", + Key: "access-type", + }, + AccessTypeParam: esmeta.SecretKeySelector{ + Name: "access-type-param-secert", + Key: "access-type-param", + }, + }, + }, + }, + }, + }, + } + err = a.framework.CRClient.Create(context.Background(), secretStore) + Expect(err).ToNot(HaveOccurred()) +} + +func (a *akeylessProvider) GetToken() (string, error) { + ctx := context.Background() + authBody := akeyless.NewAuthWithDefaults() + authBody.AccessId = akeyless.PtrString(a.accessID) + + if a.accessType == "api_key" { + authBody.AccessKey = akeyless.PtrString(a.accessTypeParam) + } else if a.accessType == "k8s" { + jwtString, err := readK8SServiceAccountJWT() + if err != nil { + return "", fmt.Errorf("failed to read JWT with Kubernetes Auth from %v. error: %w", DefServiceAccountFile, err) + } + K8SAuthConfigName := a.accessTypeParam + authBody.AccessType = akeyless.PtrString(a.accessType) + authBody.K8sServiceAccountToken = akeyless.PtrString(jwtString) + authBody.K8sAuthConfigName = akeyless.PtrString(K8SAuthConfigName) + } else { + cloudID, err := a.getCloudID(a.accessType, a.accessTypeParam) + if err != nil { + return "", fmt.Errorf("Require Cloud ID " + err.Error()) + } + authBody.AccessType = akeyless.PtrString(a.accessType) + authBody.CloudId = akeyless.PtrString(cloudID) + } + + authOut, _, err := a.restAPIClient.Auth(ctx).Body(*authBody).Execute() + if err != nil { + if errors.As(err, &apiErr) { + return "", fmt.Errorf("authentication failed: %v", string(apiErr.Body())) + } + return "", fmt.Errorf("authentication failed: %w", err) + } + + token := authOut.GetToken() + return token, nil +} + +func (a *akeylessProvider) getCloudID(provider, accTypeParam string) (string, error) { + var cloudID string + var err error + + switch provider { + case "azure_ad": + cloudID, err = azure_cloud_id.GetCloudId(accTypeParam) + case "aws_iam": + cloudID, err = aws_cloud_id.GetCloudId() + case "gcp": + cloudID, err = gcp_cloud_id.GetCloudID(accTypeParam) + default: + return "", fmt.Errorf("unable to determine provider: %s", provider) + } + return cloudID, err +} + +// readK8SServiceAccountJWT reads the JWT data for the Agent to submit to Akeyless Gateway. +func readK8SServiceAccountJWT() (string, error) { + data, err := os.Open(DefServiceAccountFile) + if err != nil { + return "", err + } + defer data.Close() + + contentBytes, err := ioutil.ReadAll(data) + if err != nil { + return "", err + } + + a := strings.TrimSpace(string(contentBytes)) + + return base64.StdEncoding.EncodeToString([]byte(a)), nil +} diff --git a/go.mod b/go.mod index 99e999af6..b1fabe3e8 100644 --- a/go.mod +++ b/go.mod @@ -33,18 +33,21 @@ replace ( ) require ( - cloud.google.com/go v0.65.0 + cloud.google.com/go v0.81.0 github.com/Azure/azure-sdk-for-go v54.1.0+incompatible github.com/Azure/go-autorest/autorest/azure/auth v0.5.7 github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect github.com/IBM/go-sdk-core/v5 v5.5.0 github.com/IBM/secrets-manager-go-sdk v1.0.23 + github.com/akeylesslabs/akeyless-go-cloud-id v0.3.2 + github.com/akeylesslabs/akeyless-go/v2 v2.5.11 github.com/aliyun/alibaba-cloud-sdk-go v1.61.1192 github.com/aws/aws-sdk-go v1.38.6 github.com/crossplane/crossplane-runtime v0.13.0 github.com/fatih/color v1.10.0 // indirect github.com/frankban/quicktest v1.10.0 // indirect + github.com/fsnotify/fsnotify v1.5.1 // indirect github.com/go-logr/logr v0.4.0 github.com/golang-jwt/jwt/v4 v4.1.0 github.com/google/go-cmp v0.5.5 @@ -73,11 +76,12 @@ require ( go.uber.org/zap v1.17.0 golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83 golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5 // indirect - golang.org/x/oauth2 v0.0.0-20210201163806-010130855d6c - golang.org/x/tools v0.1.2-0.20210512205948-8287d5da45e4 // indirect - google.golang.org/api v0.30.0 - google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a - google.golang.org/grpc v1.31.0 + golang.org/x/oauth2 v0.0.0-20210413134643-5e61552d6c78 + golang.org/x/sys v0.0.0-20211102061401-a2f17f7b995c // indirect + golang.org/x/tools v0.1.7 // indirect + google.golang.org/api v0.45.0 + google.golang.org/genproto v0.0.0-20210413151531-c14fb6ef47c3 + google.golang.org/grpc v1.37.0 honnef.co/go/tools v0.1.4 // indirect k8s.io/api v0.21.3 k8s.io/apimachinery v0.21.3 diff --git a/go.sum b/go.sum index 0085b2b71..345042c2c 100644 --- a/go.sum +++ b/go.sum @@ -11,8 +11,13 @@ cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bP cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0 h1:Dg9iHVQfrhq82rUNu9ZxUDrJLaxFUe/HlCVaLyRruq8= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= +cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= +cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= +cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= +cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= +cloud.google.com/go v0.81.0 h1:at8Tk2zUz63cLPR0JPWm5vp77pEZmzxEQBEfRKn1VV8= +cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= @@ -72,6 +77,10 @@ github.com/NYTimes/gziphandler v1.1.1/go.mod h1:n/CVRwUEOgIxrgPvAQhUUr9oeUtvrhMo github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/PuerkitoBio/purell v1.1.1/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0= github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE= +github.com/akeylesslabs/akeyless-go-cloud-id v0.3.2 h1:1h4udX3Y5KgSG0m4Th2bHfaYxZB9fbngiij9PrKEp6c= +github.com/akeylesslabs/akeyless-go-cloud-id v0.3.2/go.mod h1:ionnWiARf5TYoFGuHS7syh51/7lYosZejpZbnECFJcU= +github.com/akeylesslabs/akeyless-go/v2 v2.5.11 h1:Z7xJAUPk1h4/YGS7yr/nx9RSMaSESzPwClqe21WFYHo= +github.com/akeylesslabs/akeyless-go/v2 v2.5.11/go.mod h1:uOdXD49NCCe4rexeSc2aBU5Qv4KZgJE6YlbtYalvb+I= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -92,6 +101,8 @@ github.com/aws/aws-sdk-go v1.25.37/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpi github.com/aws/aws-sdk-go v1.34.28/go.mod h1:H7NKnBqNVzoTJpGfLrQkkD+ytBA93eiDYi/+8rV9s48= github.com/aws/aws-sdk-go v1.38.6 h1:h0AKIaz/A1kEJ50HxCv7tL1GW+KbxYbp75+lZ/nvFOI= github.com/aws/aws-sdk-go v1.38.6/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= +github.com/aws/aws-sdk-go-v2 v0.23.0 h1:+E1q1LLSfHSDn/DzOtdJOX+pLZE2HiNV2yO5AjZINwM= +github.com/aws/aws-sdk-go-v2 v0.23.0/go.mod h1:2LhT7UgHOXK3UXONKI5OMgIyoQL6zTAw/jwIeX6yqzw= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= @@ -168,8 +179,9 @@ github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoD github.com/frankban/quicktest v1.10.0 h1:Gfh+GAJZOAoKZsIZeZbdn2JF10kN1XHNvjsvQK8gVkE= github.com/frankban/quicktest v1.10.0/go.mod h1:ui7WezCLWMWxVWr1GETZY3smRy0G4KWq9vcPtJmFl7Y= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= +github.com/fsnotify/fsnotify v1.5.1 h1:mZcQUHVQUQWoPXXtuf9yuEXKudkV2sx1E06UadKWpgI= +github.com/fsnotify/fsnotify v1.5.1/go.mod h1:T3375wBYaZdLLcVNkcVbzGHY7f1l/uK5T5Ai1i3InKU= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-asn1-ber/asn1-ber v1.3.1/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= @@ -266,6 +278,7 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -281,6 +294,7 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/snappy v0.0.1 h1:Qgr9rKW7uDUkrbSmQeiDsGa8SjGyCOGtuasMWwvp2P4= @@ -295,6 +309,7 @@ github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= @@ -306,6 +321,7 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= +github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= @@ -313,6 +329,10 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= +github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -395,6 +415,7 @@ github.com/hashicorp/vault/sdk v0.1.14-0.20200519221838-e0cfd64bc267/go.mod h1:W github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA= github.com/imdario/mergo v0.3.10/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= @@ -678,7 +699,7 @@ github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/yuin/goldmark v1.4.0/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ= @@ -689,8 +710,10 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4 h1:LYy1Hy3MJdrCdMwwzxA/dRok4ejH+RwNGbuoD9fCjto= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= +go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +go.opencensus.io v0.23.0 h1:gqCw0LfLxScz8irSi8exQc7fyQ0fKQU/qnC/X8+V/1M= +go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= @@ -749,8 +772,9 @@ golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b h1:Wh+f8QHJXR411sJR8/vRBTZ7YapZaRvUcLFFJhusH0k= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5 h1:2M3HP5CCK1Si9FQhwnzYhXdG6DXeebvUHFpre8QvbyI= +golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= @@ -760,6 +784,7 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.1-0.20200828183125-ce943fd02449/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2 h1:Gz96sIWK3OalVv/I/qNygP42zyoKp3xptRVCWRFEBvo= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -802,19 +827,30 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201006153459-a7d1128ccaa0/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210224082022-3d97a244fca7/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20210201163806-010130855d6c h1:HiAZXo96zOhVhtFHchj/ojzoxCFiPrp9/j0GtS38V3g= -golang.org/x/oauth2 v0.0.0-20210201163806-010130855d6c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= +golang.org/x/oauth2 v0.0.0-20210413134643-5e61552d6c78 h1:rPRtHfUb0UKZeZ6GH4K4Nt4YRbE9V1u+QZX5upZXqJQ= +golang.org/x/oauth2 v0.0.0-20210413134643-5e61552d6c78/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -877,17 +913,26 @@ golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210412220455-f1c623a9e750/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 h1:JWgyZ1qgdTaF3N3oxC+MdTV7qvEEgHo3otj+HB5CM7Q= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/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-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20211102061401-a2f17f7b995c h1:QOfDMdrf/UwlVR0UBq2Mpr58UzNtvgJRXA4BgPfFACs= +golang.org/x/sys v0.0.0-20211102061401-a2f17f7b995c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d h1:SZxvLBoTP5yHO3Frd4z4vrF+DBX9vMVanchswa69toE= @@ -965,13 +1010,18 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= +golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= golang.org/x/tools v0.0.0-20200918232735-d647fc253266/go.mod h1:z6u4i615ZeAfBE4XtMziQW1fSVJXACjjbWkB/mvPzlU= +golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= +golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210114065538-d78b04bdf963/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.2-0.20210512205948-8287d5da45e4 h1:cYSqdOzmV9wJ7lWurRAws06Dmif0Wv6UL4gQLlz+im0= -golang.org/x/tools v0.1.2-0.20210512205948-8287d5da45e4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ= +golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -995,8 +1045,14 @@ google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0 h1:yfrXXP61wVuLb0vBcG6qaOoIoqYEzOQS8jum51jkv2w= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= +google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= +google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= +google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= +google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= +google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= +google.golang.org/api v0.45.0 h1:pqMffJFLBVUDIoYsHcqtxgQVTsmxMDpYLOc5MT4Jrww= +google.golang.org/api v0.45.0/go.mod h1:ISLIJCedJolbZvDfAk+Ctuq5hf+aJ33WgtUsfyFoLXA= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= @@ -1035,9 +1091,20 @@ google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7Fc google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201019141844-1ed22bb0c154/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a h1:pOwg4OoaRYScjmR4LlLgdtnyoHYTSAVhhqe5uPdpII8= +google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201110150050-8816d57aaa9a/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= +google.golang.org/genproto v0.0.0-20210413151531-c14fb6ef47c3 h1:K+7Ig5hjiLVA/i1UFUUbCGimWz5/Ey0lAQjT3QiLaPY= +google.golang.org/genproto v0.0.0-20210413151531-c14fb6ef47c3/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= google.golang.org/grpc v1.27.0 h1:rRYRFMVgRv6E0D70Skyfsr28tDXIuuPZyWGMPdMcnXg= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= diff --git a/hack/api-docs/mkdocs.yml b/hack/api-docs/mkdocs.yml index 32c47f0a0..c51ea7352 100644 --- a/hack/api-docs/mkdocs.yml +++ b/hack/api-docs/mkdocs.yml @@ -45,6 +45,7 @@ nav: - Secrets Manager: provider-google-secrets-manager.md - IBM: - Secrets Manager: provider-ibm-secrets-manager.md + - Akeyless: provider-akeyless.md - HashiCorp Vault: provider-hashicorp-vault.md - Yandex: - Lockbox: provider-yandex-lockbox.md diff --git a/pkg/provider/akeyless/akeyless.go b/pkg/provider/akeyless/akeyless.go new file mode 100644 index 000000000..d3912a3ef --- /dev/null +++ b/pkg/provider/akeyless/akeyless.go @@ -0,0 +1,155 @@ +/* +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 akeyless + +import ( + "context" + "encoding/json" + "fmt" + "strconv" + + "github.com/akeylesslabs/akeyless-go/v2" + "sigs.k8s.io/controller-runtime/pkg/client" + + esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1" + "github.com/external-secrets/external-secrets/pkg/provider" + "github.com/external-secrets/external-secrets/pkg/provider/schema" + "github.com/external-secrets/external-secrets/pkg/utils" +) + +const ( + defaultAPIUrl = "https://api.akeyless.io" +) + +// Provider satisfies the provider interface. +type Provider struct{} + +// akeylessBase satisfies the provider.SecretsClient interface. +type akeylessBase struct { + kube client.Client + store esv1alpha1.GenericStore + namespace string + + akeylessGwAPIURL string + RestAPI *akeyless.V2ApiService +} + +type Akeyless struct { + Client akeylessVaultInterface +} + +type akeylessVaultInterface interface { + GetSecretByType(secretName, token string, version int32) (string, error) + TokenFromSecretRef(ctx context.Context) (string, error) +} + +func init() { + schema.Register(&Provider{}, &esv1alpha1.SecretStoreProvider{ + Akeyless: &esv1alpha1.AkeylessProvider{}, + }) +} + +// NewClient constructs a new secrets client based on the provided store. +func (p *Provider) NewClient(ctx context.Context, store esv1alpha1.GenericStore, kube client.Client, namespace string) (provider.SecretsClient, error) { + return newClient(ctx, store, kube, namespace) +} + +func newClient(_ context.Context, store esv1alpha1.GenericStore, kube client.Client, namespace string) (provider.SecretsClient, error) { + akl := &akeylessBase{ + kube: kube, + store: store, + namespace: namespace, + } + + spec, err := GetAKeylessProvider(store) + if err != nil { + return nil, err + } + akeylessGwAPIURL := defaultAPIUrl + if spec != nil && spec.AkeylessGWApiURL != nil && *spec.AkeylessGWApiURL != "" { + akeylessGwAPIURL = getV2Url(*spec.AkeylessGWApiURL) + } + + if spec.Auth == nil { + return nil, fmt.Errorf("missing Auth in store config") + } + + RestAPIClient := akeyless.NewAPIClient(&akeyless.Configuration{ + Servers: []akeyless.ServerConfiguration{ + { + URL: akeylessGwAPIURL, + }, + }, + }).V2Api + + akl.akeylessGwAPIURL = akeylessGwAPIURL + akl.RestAPI = RestAPIClient + return &Akeyless{Client: akl}, nil +} + +func (a *Akeyless) Close(ctx context.Context) error { + return nil +} + +// Implements store.Client.GetSecret Interface. +// Retrieves a secret with the secret name defined in ref.Name. +func (a *Akeyless) GetSecret(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) ([]byte, error) { + if utils.IsNil(a.Client) { + return nil, fmt.Errorf(errUninitalizedAkeylessProvider) + } + + token, err := a.Client.TokenFromSecretRef(ctx) + if err != nil { + return nil, err + } + version := int32(0) + if ref.Version != "" { + i, err := strconv.ParseInt(ref.Version, 10, 32) + if err == nil { + version = int32(i) + } + } + value, err := a.Client.GetSecretByType(ref.Key, token, version) + if err != nil { + return nil, err + } + return []byte(value), nil +} + +// Implements store.Client.GetSecretMap Interface. +// New version of GetSecretMap. +func (a *Akeyless) GetSecretMap(ctx context.Context, ref esv1alpha1.ExternalSecretDataRemoteRef) (map[string][]byte, error) { + if utils.IsNil(a.Client) { + return nil, fmt.Errorf(errUninitalizedAkeylessProvider) + } + + val, err := a.GetSecret(ctx, ref) + if err != nil { + return nil, err + } + // Maps the json data to a string:string map + kv := make(map[string]string) + err = json.Unmarshal(val, &kv) + if err != nil { + return nil, fmt.Errorf(errJSONSecretUnmarshal, err) + } + + // Converts values in K:V pairs into bytes, while leaving keys as strings + secretData := make(map[string][]byte) + for k, v := range kv { + secretData[k] = []byte(v) + } + return secretData, nil +} diff --git a/pkg/provider/akeyless/akeyless_api.go b/pkg/provider/akeyless/akeyless_api.go new file mode 100644 index 000000000..f4a5b0400 --- /dev/null +++ b/pkg/provider/akeyless/akeyless_api.go @@ -0,0 +1,250 @@ +/* +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 akeyless + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "os" + "strings" + + aws_cloud_id "github.com/akeylesslabs/akeyless-go-cloud-id/cloudprovider/aws" + azure_cloud_id "github.com/akeylesslabs/akeyless-go-cloud-id/cloudprovider/azure" + gcp_cloud_id "github.com/akeylesslabs/akeyless-go-cloud-id/cloudprovider/gcp" + "github.com/akeylesslabs/akeyless-go/v2" +) + +var apiErr akeyless.GenericOpenAPIError + +const DefServiceAccountFile = "/var/run/secrets/kubernetes.io/serviceaccount/token" + +func (a *akeylessBase) GetToken(accessID, accType, accTypeParam string) (string, error) { + ctx := context.Background() + authBody := akeyless.NewAuthWithDefaults() + authBody.AccessId = akeyless.PtrString(accessID) + if accType == "api_key" || accType == "access_key" { + authBody.AccessKey = akeyless.PtrString(accTypeParam) + } else if accType == "k8s" { + jwtString, err := readK8SServiceAccountJWT() + if err != nil { + return "", fmt.Errorf("failed to read JWT with Kubernetes Auth from %v. error: %w", DefServiceAccountFile, err) + } + K8SAuthConfigName := accTypeParam + authBody.AccessType = akeyless.PtrString(accType) + authBody.K8sServiceAccountToken = akeyless.PtrString(jwtString) + authBody.K8sAuthConfigName = akeyless.PtrString(K8SAuthConfigName) + } else { + cloudID, err := a.getCloudID(accType, accTypeParam) + if err != nil { + return "", fmt.Errorf("Require Cloud ID " + err.Error()) + } + authBody.AccessType = akeyless.PtrString(accType) + authBody.CloudId = akeyless.PtrString(cloudID) + } + + authOut, _, err := a.RestAPI.Auth(ctx).Body(*authBody).Execute() + if err != nil { + if errors.As(err, &apiErr) { + return "", fmt.Errorf("authentication failed: %v", string(apiErr.Body())) + } + return "", fmt.Errorf("authentication failed: %w", err) + } + + token := authOut.GetToken() + return token, nil +} + +func (a *akeylessBase) GetSecretByType(secretName, token string, version int32) (string, error) { + item, err := a.DescribeItem(secretName, token) + if err != nil { + return "", err + } + secretType := item.GetItemType() + + switch secretType { + case "STATIC_SECRET": + return a.GetStaticSecret(secretName, token, version) + case "DYNAMIC_SECRET": + return a.GetDynamicSecrets(secretName, token) + case "ROTATED_SECRET": + return a.GetRotatedSecrets(secretName, token, version) + default: + return "", fmt.Errorf("invalid item type: %v", secretType) + } +} + +func (a *akeylessBase) DescribeItem(itemName, token string) (*akeyless.Item, error) { + ctx := context.Background() + + body := akeyless.DescribeItem{ + Name: itemName, + } + if strings.HasPrefix(token, "u-") { + body.UidToken = &token + } else { + body.Token = &token + } + gsvOut, _, err := a.RestAPI.DescribeItem(ctx).Body(body).Execute() + if err != nil { + if errors.As(err, &apiErr) { + return nil, fmt.Errorf("can't describe item: %v", string(apiErr.Body())) + } + return nil, fmt.Errorf("can't describe item: %w", err) + } + + return &gsvOut, nil +} + +func (a *akeylessBase) GetRotatedSecrets(secretName, token string, version int32) (string, error) { + ctx := context.Background() + + body := akeyless.GetRotatedSecretValue{ + Names: secretName, + Version: &version, + } + if strings.HasPrefix(token, "u-") { + body.UidToken = &token + } else { + body.Token = &token + } + + gsvOut, _, err := a.RestAPI.GetRotatedSecretValue(ctx).Body(body).Execute() + if err != nil { + if errors.As(err, &apiErr) { + return "", fmt.Errorf("can't get rotated secret value: %v", string(apiErr.Body())) + } + return "", fmt.Errorf("can't get rotated secret value: %w", err) + } + + val, ok := gsvOut["value"] + if ok { + if _, ok := val["payload"]; ok { + return fmt.Sprintf("%v", val["payload"]), nil + } else if _, ok := val["target_value"]; ok { + out, err := json.Marshal(val["target_value"]) + if err != nil { + return "", fmt.Errorf("can't marshal rotated secret value: %w", err) + } + return string(out), nil + } else { + out, err := json.Marshal(val) + if err != nil { + return "", fmt.Errorf("can't marshal rotated secret value: %w", err) + } + return string(out), nil + } + } + out, err := json.Marshal(gsvOut) + if err != nil { + return "", fmt.Errorf("can't marshal rotated secret value: %w", err) + } + return string(out), nil +} + +func (a *akeylessBase) GetDynamicSecrets(secretName, token string) (string, error) { + ctx := context.Background() + + body := akeyless.GetDynamicSecretValue{ + Name: secretName, + } + if strings.HasPrefix(token, "u-") { + body.UidToken = &token + } else { + body.Token = &token + } + + gsvOut, _, err := a.RestAPI.GetDynamicSecretValue(ctx).Body(body).Execute() + if err != nil { + if errors.As(err, &apiErr) { + return "", fmt.Errorf("can't get dynamic secret value: %v", string(apiErr.Body())) + } + return "", fmt.Errorf("can't get dynamic secret value: %w", err) + } + + out, err := json.Marshal(gsvOut) + if err != nil { + return "", fmt.Errorf("can't marshal dynamic secret value: %w", err) + } + + return string(out), nil +} + +func (a *akeylessBase) GetStaticSecret(secretName, token string, version int32) (string, error) { + ctx := context.Background() + + gsvBody := akeyless.GetSecretValue{ + Names: []string{secretName}, + Version: &version, + } + + if strings.HasPrefix(token, "u-") { + gsvBody.UidToken = &token + } else { + gsvBody.Token = &token + } + + gsvOut, _, err := a.RestAPI.GetSecretValue(ctx).Body(gsvBody).Execute() + if err != nil { + if errors.As(err, &apiErr) { + return "", fmt.Errorf("can't get secret value: %v", string(apiErr.Body())) + } + return "", fmt.Errorf("can't get secret value: %w", err) + } + val, ok := gsvOut[secretName] + if !ok { + return "", fmt.Errorf("can't get secret: %v", secretName) + } + + return val, nil +} + +func (a *akeylessBase) getCloudID(provider, accTypeParam string) (string, error) { + var cloudID string + var err error + + switch provider { + case "azure_ad": + cloudID, err = azure_cloud_id.GetCloudId(accTypeParam) + case "aws_iam": + cloudID, err = aws_cloud_id.GetCloudId() + case "gcp": + cloudID, err = gcp_cloud_id.GetCloudID(accTypeParam) + default: + return "", fmt.Errorf("unable to determine provider: %s", provider) + } + return cloudID, err +} + +// readK8SServiceAccountJWT reads the JWT data for the Agent to submit to Akeyless Gateway. +func readK8SServiceAccountJWT() (string, error) { + data, err := os.Open(DefServiceAccountFile) + if err != nil { + return "", err + } + defer data.Close() + + contentBytes, err := ioutil.ReadAll(data) + if err != nil { + return "", err + } + + a := strings.TrimSpace(string(contentBytes)) + + return base64.StdEncoding.EncodeToString([]byte(a)), nil +} diff --git a/pkg/provider/akeyless/akeyless_test.go b/pkg/provider/akeyless/akeyless_test.go new file mode 100644 index 000000000..8b1585d02 --- /dev/null +++ b/pkg/provider/akeyless/akeyless_test.go @@ -0,0 +1,168 @@ +/* +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 akeyless + +import ( + "context" + "fmt" + "reflect" + "strings" + "testing" + + esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1" + fakeakeyless "github.com/external-secrets/external-secrets/pkg/provider/akeyless/fake" +) + +type akeylessTestCase struct { + mockClient *fakeakeyless.AkeylessMockClient + apiInput *fakeakeyless.Input + apiOutput *fakeakeyless.Output + ref *esv1alpha1.ExternalSecretDataRemoteRef + expectError string + expectedSecret string + // for testing secretmap + expectedData map[string][]byte +} + +func makeValidAkeylessTestCase() *akeylessTestCase { + smtc := akeylessTestCase{ + mockClient: &fakeakeyless.AkeylessMockClient{}, + apiInput: makeValidInput(), + ref: makeValidRef(), + apiOutput: makeValidOutput(), + expectError: "", + expectedSecret: "", + expectedData: map[string][]byte{}, + } + smtc.mockClient.WithValue(smtc.apiInput, smtc.apiOutput) + return &smtc +} + +func makeValidRef() *esv1alpha1.ExternalSecretDataRemoteRef { + return &esv1alpha1.ExternalSecretDataRemoteRef{ + Key: "test-secret", + Version: "1", + } +} + +func makeValidInput() *fakeakeyless.Input { + return &fakeakeyless.Input{ + SecretName: "name", + Version: 0, + Token: "token", + } +} + +func makeValidOutput() *fakeakeyless.Output { + return &fakeakeyless.Output{ + Value: "secret-val", + Err: nil, + } +} + +func makeValidAkeylessTestCaseCustom(tweaks ...func(smtc *akeylessTestCase)) *akeylessTestCase { + smtc := makeValidAkeylessTestCase() + for _, fn := range tweaks { + fn(smtc) + } + smtc.mockClient.WithValue(smtc.apiInput, smtc.apiOutput) + return smtc +} + +// This case can be shared by both GetSecret and GetSecretMap tests. +// bad case: set apiErr. +var setAPIErr = func(smtc *akeylessTestCase) { + smtc.apiOutput.Err = fmt.Errorf("oh no") + smtc.expectError = "oh no" +} + +var setNilMockClient = func(smtc *akeylessTestCase) { + smtc.mockClient = nil + smtc.expectError = errUninitalizedAkeylessProvider +} + +func TestAkeylessGetSecret(t *testing.T) { + secretValue := "changedvalue" + // good case: default version is set + // key is passed in, output is sent back + setSecretString := func(smtc *akeylessTestCase) { + smtc.apiOutput = &fakeakeyless.Output{ + Value: secretValue, + Err: nil, + } + smtc.expectedSecret = secretValue + } + + successCases := []*akeylessTestCase{ + makeValidAkeylessTestCaseCustom(setAPIErr), + makeValidAkeylessTestCaseCustom(setSecretString), + makeValidAkeylessTestCaseCustom(setNilMockClient), + } + + sm := Akeyless{} + for k, v := range successCases { + sm.Client = v.mockClient + fmt.Println(*v.ref) + out, err := sm.GetSecret(context.Background(), *v.ref) + if !ErrorContains(err, v.expectError) { + t.Errorf("[%d] unexpected error: %s, expected: '%s'", k, err.Error(), v.expectError) + } + if string(out) != v.expectedSecret { + t.Errorf("[%d] unexpected secret: expected %s, got %s", k, v.expectedSecret, string(out)) + } + } +} + +func TestGetSecretMap(t *testing.T) { + // good case: default version & deserialization + setDeserialization := func(smtc *akeylessTestCase) { + smtc.apiOutput.Value = `{"foo":"bar"}` + smtc.expectedData["foo"] = []byte("bar") + } + + // bad case: invalid json + setInvalidJSON := func(smtc *akeylessTestCase) { + smtc.apiOutput.Value = `-----------------` + smtc.expectError = "unable to unmarshal secret" + } + + successCases := []*akeylessTestCase{ + makeValidAkeylessTestCaseCustom(setDeserialization), + makeValidAkeylessTestCaseCustom(setInvalidJSON), + makeValidAkeylessTestCaseCustom(setAPIErr), + makeValidAkeylessTestCaseCustom(setNilMockClient), + } + + sm := Akeyless{} + for k, v := range successCases { + sm.Client = v.mockClient + out, err := sm.GetSecretMap(context.Background(), *v.ref) + if !ErrorContains(err, v.expectError) { + t.Errorf("[%d] unexpected error: %s, expected: '%s'", k, err.Error(), v.expectError) + } + if err == nil && !reflect.DeepEqual(out, v.expectedData) { + t.Errorf("[%d] unexpected secret data: expected %#v, got %#v", k, v.expectedData, out) + } + } +} + +func ErrorContains(out error, want string) bool { + if out == nil { + return want == "" + } + if want == "" { + return false + } + return strings.Contains(out.Error(), want) +} diff --git a/pkg/provider/akeyless/auth.go b/pkg/provider/akeyless/auth.go new file mode 100644 index 000000000..f28465c3a --- /dev/null +++ b/pkg/provider/akeyless/auth.go @@ -0,0 +1,103 @@ +/* +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 akeyless + +import ( + "context" + "fmt" + + v1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + + esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1" +) + +const ( + errInvalidClusterStoreMissingAKIDNamespace = "invalid ClusterSecretStore: missing Akeyless AccessID Namespace" + errInvalidClusterStoreMissingSAKNamespace = "invalid ClusterSecretStore: missing Akeyless AccessType Namespace" + errFetchAKIDSecret = "could not fetch accessID secret: %w" + errFetchSAKSecret = "could not fetch AccessType secret: %w" + errMissingSAK = "missing SecretAccessKey" + errMissingAKID = "missing AccessKeyID" +) + +func (a *akeylessBase) TokenFromSecretRef(ctx context.Context) (string, error) { + prov, err := GetAKeylessProvider(a.store) + if err != nil { + return "", err + } + + ke := client.ObjectKey{ + Name: prov.Auth.SecretRef.AccessID.Name, + Namespace: a.namespace, // default to ExternalSecret namespace + } + // only ClusterStore is allowed to set namespace (and then it's required) + if a.store.GetObjectKind().GroupVersionKind().Kind == esv1alpha1.ClusterSecretStoreKind { + if prov.Auth.SecretRef.AccessID.Namespace == nil { + return "", fmt.Errorf(errInvalidClusterStoreMissingAKIDNamespace) + } + ke.Namespace = *prov.Auth.SecretRef.AccessID.Namespace + } + accessIDSecret := v1.Secret{} + err = a.kube.Get(ctx, ke, &accessIDSecret) + if err != nil { + return "", fmt.Errorf(errFetchAKIDSecret, err) + } + ke = client.ObjectKey{ + Name: prov.Auth.SecretRef.AccessType.Name, + Namespace: a.namespace, // default to ExternalSecret namespace + } + // only ClusterStore is allowed to set namespace (and then it's required) + if a.store.GetObjectKind().GroupVersionKind().Kind == esv1alpha1.ClusterSecretStoreKind { + if prov.Auth.SecretRef.AccessType.Namespace == nil { + return "", fmt.Errorf(errInvalidClusterStoreMissingSAKNamespace) + } + ke.Namespace = *prov.Auth.SecretRef.AccessType.Namespace + } + accessTypeSecret := v1.Secret{} + err = a.kube.Get(ctx, ke, &accessTypeSecret) + if err != nil { + return "", fmt.Errorf(errFetchSAKSecret, err) + } + + ke = client.ObjectKey{ + Name: prov.Auth.SecretRef.AccessTypeParam.Name, + Namespace: a.namespace, // default to ExternalSecret namespace + } + // only ClusterStore is allowed to set namespace (and then it's required) + if a.store.GetObjectKind().GroupVersionKind().Kind == esv1alpha1.ClusterSecretStoreKind { + if prov.Auth.SecretRef.AccessType.Namespace == nil { + return "", fmt.Errorf(errInvalidClusterStoreMissingSAKNamespace) + } + ke.Namespace = *prov.Auth.SecretRef.AccessType.Namespace + } + accessTypeParamSecret := v1.Secret{} + err = a.kube.Get(ctx, ke, &accessTypeParamSecret) + if err != nil { + return "", fmt.Errorf(errFetchSAKSecret, err) + } + accessID := string(accessIDSecret.Data[prov.Auth.SecretRef.AccessID.Key]) + accessType := string(accessTypeSecret.Data[prov.Auth.SecretRef.AccessType.Key]) + accessTypeParam := string(accessTypeSecret.Data[prov.Auth.SecretRef.AccessTypeParam.Key]) + + if accessID == "" { + return "", fmt.Errorf(errMissingSAK) + } + if accessType == "" { + return "", fmt.Errorf(errMissingAKID) + } + + return a.GetToken(accessID, accessType, accessTypeParam) +} diff --git a/pkg/provider/akeyless/fake/fake.go b/pkg/provider/akeyless/fake/fake.go new file mode 100644 index 000000000..f93185b8e --- /dev/null +++ b/pkg/provider/akeyless/fake/fake.go @@ -0,0 +1,49 @@ +/* +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 fake + +import ( + "context" +) + +type AkeylessMockClient struct { + getSecret func(secretName, token string, version int32) (string, error) +} + +func (mc *AkeylessMockClient) TokenFromSecretRef(ctx context.Context) (string, error) { + return "newToken", nil +} + +func (mc *AkeylessMockClient) GetSecretByType(secretName, token string, version int32) (string, error) { + return mc.getSecret(secretName, token, version) +} + +func (mc *AkeylessMockClient) WithValue(in *Input, out *Output) { + if mc != nil { + mc.getSecret = func(secretName, token string, version int32) (string, error) { + return out.Value, out.Err + } + } +} + +type Input struct { + SecretName string + Token string + Version int32 +} + +type Output struct { + Value string + Err error +} diff --git a/pkg/provider/akeyless/utils.go b/pkg/provider/akeyless/utils.go new file mode 100644 index 000000000..a6d52de06 --- /dev/null +++ b/pkg/provider/akeyless/utils.go @@ -0,0 +1,99 @@ +/* +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 akeyless + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/url" + "strings" + "time" + + esv1alpha1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1alpha1" +) + +const ( + errNilStore = "found nil store" + errMissingStoreSpec = "store is missing spec" + errMissingProvider = "storeSpec is missing provider" + errInvalidProvider = "invalid provider spec. Missing Akeyless field in store %s" + errJSONSecretUnmarshal = "unable to unmarshal secret: %w" + errUninitalizedAkeylessProvider = "provider akeyless is not initialized" +) + +// GetAKeylessProvider does the necessary nil checks and returns the akeyless provider or an error. +func GetAKeylessProvider(store esv1alpha1.GenericStore) (*esv1alpha1.AkeylessProvider, error) { + if store == nil { + return nil, fmt.Errorf(errNilStore) + } + spc := store.GetSpec() + if spc == nil { + return nil, fmt.Errorf(errMissingStoreSpec) + } + if spc.Provider == nil { + return nil, fmt.Errorf(errMissingProvider) + } + prov := spc.Provider.Akeyless + if prov == nil { + return nil, fmt.Errorf(errInvalidProvider, store.GetObjectMeta().String()) + } + return prov, nil +} + +func getV2Url(path string) string { + // add check if not v2 + rebody := sendReq(path) + if strings.Contains(rebody, "unknown command") { + return path + } + + if strings.HasSuffix(path, "/v2") { + return path + } + url, err := url.Parse(path) + if err != nil { + return path + } + if strings.HasSuffix(url.Host, "/v2") { + return path + } + url.Host += "/v2" + p := url.Scheme + "://" + url.Host + if url.Port() != "" { + p = p + ":" + url.Port() + } + + return p +} + +func sendReq(url string) string { + req, err := http.NewRequest("POST", url, nil) + if err != nil { + return "" + } + req.Header.Set("Content-Type", "application/json") + + client := &http.Client{ + Timeout: 10 * time.Second, + } + resp, err := client.Do(req) + if err != nil { + return "" + } + defer resp.Body.Close() + + body, _ := ioutil.ReadAll(resp.Body) + return string(body) +} diff --git a/pkg/provider/register/register.go b/pkg/provider/register/register.go index 4061515e9..08f52e2a0 100644 --- a/pkg/provider/register/register.go +++ b/pkg/provider/register/register.go @@ -17,6 +17,7 @@ package register // packages imported here are registered to the controller schema. // nolint:revive import ( + _ "github.com/external-secrets/external-secrets/pkg/provider/akeyless" _ "github.com/external-secrets/external-secrets/pkg/provider/alibaba" _ "github.com/external-secrets/external-secrets/pkg/provider/aws" _ "github.com/external-secrets/external-secrets/pkg/provider/azure/keyvault"