get.porter.sh/porter@v1.3.0/pkg/storage/credentialset.go (about)

     1  package storage
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"fmt"
     7  	"strings"
     8  	"time"
     9  
    10  	"get.porter.sh/porter/pkg/cnab"
    11  	"get.porter.sh/porter/pkg/schema"
    12  	"get.porter.sh/porter/pkg/secrets"
    13  	"get.porter.sh/porter/pkg/tracing"
    14  	"github.com/cnabio/cnab-go/bundle"
    15  	"github.com/cnabio/cnab-go/valuesource"
    16  	"go.opentelemetry.io/otel/attribute"
    17  )
    18  
    19  var _ Document = CredentialSet{}
    20  
    21  // CredentialSet defines mappings from a credential needed by a bundle to where
    22  // to look for it when the bundle is run. For example: Bundle needs Azure
    23  // storage connection string, and it should look for it in an environment
    24  // variable named `AZURE_STORATE_CONNECTION_STRING` or a key named `dev-conn`.
    25  //
    26  // Porter discourages storing the value of the credential directly, though
    27  // it is possible. Instead, Porter encourages the best practice of defining
    28  // mappings in the credential sets, and then storing the values in secret stores
    29  // such as a key/value store like Hashicorp Vault, or Azure Key Vault.
    30  // See the get.porter.sh/porter/pkg/secrets package for more on how Porter
    31  // handles accessing secrets.
    32  type CredentialSet struct {
    33  	CredentialSetSpec `yaml:",inline"`
    34  	Status            CredentialSetStatus `json:"status,omitempty" yaml:"status,omitempty" toml:"status,omitempty"`
    35  }
    36  
    37  // CredentialSetSpec represents the set of user-modifiable fields on a CredentialSet.
    38  type CredentialSetSpec struct {
    39  	// SchemaType is the type of resource in the current document.
    40  	SchemaType string `json:"schemaType,omitempty" yaml:"schemaType,omitempty" toml:"schemaType,omitempty"`
    41  
    42  	// SchemaVersion is the version of the credential-set schema.
    43  	SchemaVersion cnab.SchemaVersion `json:"schemaVersion" yaml:"schemaVersion" toml:"schemaVersion"`
    44  
    45  	// Namespace to which the credential set is scoped.
    46  	Namespace string `json:"namespace" yaml:"namespace" toml:"namespace"`
    47  
    48  	// Name of the credential set.
    49  	Name string `json:"name" yaml:"name" toml:"name"`
    50  
    51  	// Labels applied to the credential set.
    52  	Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty" toml:"labels,omitempty"`
    53  
    54  	// Credentials is a list of credential resolution strategies.
    55  	Credentials secrets.StrategyList `json:"credentials,omitempty" yaml:"credentials,omitempty" toml:"credentials,omitempty"`
    56  }
    57  
    58  // We implement a custom json marshal instead of using tags, so that we can omit zero-value timestamps
    59  var _ json.Marshaler = CredentialSetStatus{}
    60  
    61  // CredentialSetStatus contains additional status metadata that has been set by Porter.
    62  type CredentialSetStatus struct {
    63  	// Created timestamp.
    64  	Created time.Time `json:"created,omitempty" yaml:"created,omitempty" toml:"created,omitempty"`
    65  
    66  	// Modified timestamp.
    67  	Modified time.Time `json:"modified,omitempty" yaml:"modified,omitempty" toml:"modified,omitempty"`
    68  }
    69  
    70  func NewInternalCredentialSet(creds ...secrets.SourceMap) CredentialSet {
    71  	return CredentialSet{
    72  		CredentialSetSpec: CredentialSetSpec{Credentials: creds},
    73  	}
    74  }
    75  
    76  // NewCredentialSet creates a new CredentialSet with the required fields initialized.
    77  func NewCredentialSet(namespace string, name string, creds ...secrets.SourceMap) CredentialSet {
    78  	now := time.Now()
    79  	cs := CredentialSet{
    80  		CredentialSetSpec: CredentialSetSpec{
    81  			SchemaType:    SchemaTypeCredentialSet,
    82  			SchemaVersion: DefaultCredentialSetSchemaVersion,
    83  			Name:          name,
    84  			Namespace:     namespace,
    85  			Credentials:   creds,
    86  		},
    87  		Status: CredentialSetStatus{
    88  			Created:  now,
    89  			Modified: now,
    90  		},
    91  	}
    92  
    93  	return cs
    94  }
    95  
    96  func (c CredentialSetStatus) MarshalJSON() ([]byte, error) {
    97  	raw := make(map[string]interface{}, 2)
    98  	if !c.Created.IsZero() {
    99  		raw["created"] = c.Created
   100  	}
   101  	if !c.Modified.IsZero() {
   102  		raw["modified"] = c.Modified
   103  	}
   104  	return json.Marshal(raw)
   105  }
   106  
   107  func (s CredentialSet) DefaultDocumentFilter() map[string]interface{} {
   108  	return map[string]interface{}{"namespace": s.Namespace, "name": s.Name}
   109  }
   110  
   111  func (s *CredentialSet) Validate(ctx context.Context, strategy schema.CheckStrategy) error {
   112  	_, span := tracing.StartSpan(ctx,
   113  		attribute.String("credentialset", s.String()),
   114  		attribute.String("schemaVersion", string(s.SchemaVersion)),
   115  		attribute.String("defaultSchemaVersion", string(DefaultCredentialSetSchemaVersion)))
   116  	defer span.EndSpan()
   117  
   118  	// Before we can validate, get our resource in a consistent state
   119  	// 1. Check if we know what to do with this version of the resource
   120  	if warnOnly, err := schema.ValidateSchemaVersion(strategy, SupportedCredentialSetSchemaVersions, string(s.SchemaVersion), DefaultCredentialSetSemverSchemaVersion); err != nil {
   121  		if warnOnly {
   122  			span.Warn(err.Error())
   123  		} else {
   124  			return span.Error(err)
   125  		}
   126  	}
   127  
   128  	// 2. Check if they passed in the right resource type
   129  	if s.SchemaType != "" && !strings.EqualFold(s.SchemaType, SchemaTypeCredentialSet) {
   130  		return span.Errorf("invalid schemaType %s, expected %s", s.SchemaType, SchemaTypeCredentialSet)
   131  	}
   132  
   133  	// Default the schemaType before importing into the database if it's not set already
   134  	// SchemaType isn't really used by our code, it's a type hint for editors, but this will ensure we are consistent in our persisted documents
   135  	if s.SchemaType == "" {
   136  		s.SchemaType = SchemaTypeCredentialSet
   137  	}
   138  
   139  	// OK! Now we can do resource specific validations
   140  	return nil
   141  }
   142  
   143  func (s CredentialSet) String() string {
   144  	return fmt.Sprintf("%s/%s", s.Namespace, s.Name)
   145  }
   146  
   147  // ToCNAB converts this to a type accepted by the cnab-go runtime.
   148  func (s CredentialSet) ToCNAB() valuesource.Set {
   149  	values := make(valuesource.Set, len(s.Credentials))
   150  	for _, cred := range s.Credentials {
   151  		values[cred.Name] = cred.ResolvedValue
   152  	}
   153  	return values
   154  }
   155  
   156  // HasCredential determines if the specified credential is defined in the set.
   157  func (s CredentialSet) HasCredential(name string) bool {
   158  	for _, cred := range s.Credentials {
   159  		if cred.Name == name {
   160  			return true
   161  		}
   162  	}
   163  
   164  	return false
   165  }
   166  
   167  // Validate compares the given credentials with the spec.
   168  //
   169  // This will result in an error only when the following conditions are true:
   170  // - a credential in the spec is not present in the given set
   171  // - the credential is required
   172  // - the credential applies to the specified action
   173  //
   174  // It is allowed for spec to specify both an env var and a file. In such case, if
   175  // the given set provides either, it will be considered valid.
   176  func (s CredentialSet) ValidateBundle(spec map[string]bundle.Credential, action string) error {
   177  	for name, cred := range spec {
   178  		if !cred.AppliesTo(action) {
   179  			continue
   180  		}
   181  
   182  		if !s.HasCredential(name) && cred.Required {
   183  			return fmt.Errorf("bundle requires credential for %s", name)
   184  		}
   185  	}
   186  	return nil
   187  }
   188  
   189  // Keys returns the names of all the credentials in the set.
   190  func (s CredentialSet) Keys() []string {
   191  	keys := make([]string, 0, len(s.Credentials))
   192  	for _, cred := range s.Credentials {
   193  		keys = append(keys, cred.Name)
   194  	}
   195  	return keys
   196  }
   197  
   198  // GetCredential returns the credential with the given name, and a boolean indicating if it was found.
   199  func (s CredentialSet) GetCredential(name string) (*secrets.SourceMap, bool) {
   200  	for _, cred := range s.Credentials {
   201  		if cred.Name == name {
   202  			return &cred, true
   203  		}
   204  	}
   205  	return nil, false
   206  }