| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196 |
- /*
- Copyright © The ESO Authors
- 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
- https://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 github
- import (
- "context"
- crypto_rand "crypto/rand"
- "encoding/base64"
- "encoding/json"
- "fmt"
- "time"
- github "github.com/google/go-github/v56/github"
- "golang.org/x/crypto/nacl/box"
- corev1 "k8s.io/api/core/v1"
- "sigs.k8s.io/controller-runtime/pkg/client"
- esv1 "github.com/external-secrets/external-secrets/apis/externalsecrets/v1"
- )
- const errWriteOnlyProvider = "not implemented - this provider supports write-only operations"
- // https://github.com/external-secrets/external-secrets/issues/644
- var _ esv1.SecretsClient = &Client{}
- // ActionsServiceClient defines the interface for interacting with GitHub Actions secrets.
- type ActionsServiceClient interface {
- // CreateOrUpdateOrgSecret creates or updates an organization secret.
- CreateOrUpdateOrgSecret(ctx context.Context, org string, eSecret *github.EncryptedSecret) (response *github.Response, err error)
- // GetOrgSecret retrieves an organization secret.
- GetOrgSecret(ctx context.Context, org string, name string) (*github.Secret, *github.Response, error)
- // ListOrgSecrets lists all organization secrets.
- ListOrgSecrets(ctx context.Context, org string, opts *github.ListOptions) (*github.Secrets, *github.Response, error)
- }
- // Client implements the External Secrets Kubernetes provider for GitHub Actions secrets.
- type Client struct {
- crClient client.Client
- store esv1.GenericStore
- provider *esv1.GithubProvider
- baseClient github.ActionsService
- namespace string
- storeKind string
- repoID int64
- getSecretFn func(ctx context.Context, ref esv1.PushSecretRemoteRef) (*github.Secret, *github.Response, error)
- getPublicKeyFn func(ctx context.Context) (*github.PublicKey, *github.Response, error)
- createOrUpdateFn func(ctx context.Context, eSecret *github.EncryptedSecret) (*github.Response, error)
- listSecretsFn func(ctx context.Context) (*github.Secrets, *github.Response, error)
- deleteSecretFn func(ctx context.Context, ref esv1.PushSecretRemoteRef) (*github.Response, error)
- }
- // DeleteSecret deletes a secret from GitHub Actions.
- func (g *Client) DeleteSecret(ctx context.Context, remoteRef esv1.PushSecretRemoteRef) error {
- _, err := g.deleteSecretFn(ctx, remoteRef)
- if err != nil {
- return fmt.Errorf("failed to delete secret: %w", err)
- }
- return nil
- }
- // SecretExists checks if a secret exists in GitHub Actions.
- func (g *Client) SecretExists(ctx context.Context, ref esv1.PushSecretRemoteRef) (bool, error) {
- githubSecret, _, err := g.getSecretFn(ctx, ref)
- if err != nil {
- return false, fmt.Errorf("error fetching secret: %w", err)
- }
- if githubSecret != nil {
- return true, nil
- }
- return false, nil
- }
- // PushSecret pushes a new secret to GitHub Actions.
- func (g *Client) PushSecret(ctx context.Context, secret *corev1.Secret, remoteRef esv1.PushSecretData) error {
- githubSecret, response, err := g.getSecretFn(ctx, remoteRef)
- if err != nil && (response == nil || response.StatusCode != 404) {
- return fmt.Errorf("error fetching secret: %w", err)
- }
- // First at all, we need the organization public key to encrypt the secret.
- publicKey, _, err := g.getPublicKeyFn(ctx)
- if err != nil {
- return fmt.Errorf("error fetching public key: %w", err)
- }
- decodedPublicKey, err := base64.StdEncoding.DecodeString(publicKey.GetKey())
- if err != nil {
- return fmt.Errorf("unable to decode public key: %w", err)
- }
- var boxKey [32]byte
- copy(boxKey[:], decodedPublicKey)
- var ok bool
- // default to full secret.
- value, err := json.Marshal(secret.Data)
- if err != nil {
- return fmt.Errorf("json.Marshal failed with error %w", err)
- }
- // if key is specified, overwrite to key only
- if remoteRef.GetSecretKey() != "" {
- value, ok = secret.Data[remoteRef.GetSecretKey()]
- if !ok {
- return fmt.Errorf("key %s not found in secret", remoteRef.GetSecretKey())
- }
- }
- encryptedBytes, err := box.SealAnonymous([]byte{}, value, &boxKey, crypto_rand.Reader)
- if err != nil {
- return fmt.Errorf("box.SealAnonymous failed with error %w", err)
- }
- name := remoteRef.GetRemoteKey()
- visibility := g.resolveOrgSecretVisibility(githubSecret)
- if githubSecret != nil {
- name = githubSecret.Name
- }
- encryptedString := base64.StdEncoding.EncodeToString(encryptedBytes)
- keyID := publicKey.GetKeyID()
- encryptedSecret := &github.EncryptedSecret{
- Name: name,
- KeyID: keyID,
- EncryptedValue: encryptedString,
- Visibility: visibility,
- }
- if _, err := g.createOrUpdateFn(ctx, encryptedSecret); err != nil {
- return fmt.Errorf("failed to create secret: %w", err)
- }
- return nil
- }
- // resolveOrgSecretVisibility returns the visibility to use when creating or updating an org secret.
- //
- // Rules:
- // - If OrgSecretVisibility is set on the provider, that value is always used.
- // - Otherwise, if the secret already exists in GitHub, its current visibility is preserved.
- // - Otherwise (new secret, no provider override), visibility defaults to "all".
- func (g *Client) resolveOrgSecretVisibility(existing *github.Secret) string {
- if g.provider != nil && g.provider.OrgSecretVisibility != "" {
- return g.provider.OrgSecretVisibility
- }
- if existing != nil && existing.Visibility != "" {
- return existing.Visibility
- }
- return "all"
- }
- // GetAllSecrets is not implemented as this provider is write-only.
- func (g *Client) GetAllSecrets(_ context.Context, _ esv1.ExternalSecretFind) (map[string][]byte, error) {
- return nil, fmt.Errorf(errWriteOnlyProvider)
- }
- // GetSecret is not implemented as this provider is write-only.
- func (g *Client) GetSecret(_ context.Context, _ esv1.ExternalSecretDataRemoteRef) ([]byte, error) {
- return nil, fmt.Errorf(errWriteOnlyProvider)
- }
- // GetSecretMap is not implemented as this provider is write-only.
- func (g *Client) GetSecretMap(_ context.Context, _ esv1.ExternalSecretDataRemoteRef) (map[string][]byte, error) {
- return nil, fmt.Errorf(errWriteOnlyProvider)
- }
- // Close cleans up any resources held by the client. No-op for this provider.
- func (g *Client) Close(_ context.Context) error {
- return nil
- }
- // Validate checks if the client is properly configured and has access to the GitHub Actions API.
- func (g *Client) Validate() (esv1.ValidationResult, error) {
- if g.store.GetKind() == esv1.ClusterSecretStoreKind {
- return esv1.ValidationResultUnknown, nil
- }
- ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
- defer cancel()
- _, _, err := g.listSecretsFn(ctx)
- if err != nil {
- return esv1.ValidationResultError, fmt.Errorf("store is not allowed to list secrets: %w", err)
- }
- return esv1.ValidationResultReady, nil
- }
|