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 }