github.com/outbrain/consul@v1.4.5/agent/structs/intention.go (about) 1 package structs 2 3 import ( 4 "fmt" 5 "strconv" 6 "strings" 7 "time" 8 9 "github.com/hashicorp/consul/agent/cache" 10 "github.com/hashicorp/go-multierror" 11 "github.com/mitchellh/hashstructure" 12 ) 13 14 const ( 15 // IntentionWildcard is the wildcard value. 16 IntentionWildcard = "*" 17 18 // IntentionDefaultNamespace is the default namespace value. 19 // NOTE(mitchellh): This is only meant to be a temporary constant. 20 // When namespaces are introduced, we should delete this constant and 21 // fix up all the places where this was used with the proper namespace 22 // value. 23 IntentionDefaultNamespace = "default" 24 ) 25 26 // Intention defines an intention for the Connect Service Graph. This defines 27 // the allowed or denied behavior of a connection between two services using 28 // Connect. 29 type Intention struct { 30 // ID is the UUID-based ID for the intention, always generated by Consul. 31 ID string 32 33 // Description is a human-friendly description of this intention. 34 // It is opaque to Consul and is only stored and transferred in API 35 // requests. 36 Description string 37 38 // SourceNS, SourceName are the namespace and name, respectively, of 39 // the source service. Either of these may be the wildcard "*", but only 40 // the full value can be a wildcard. Partial wildcards are not allowed. 41 // The source may also be a non-Consul service, as specified by SourceType. 42 // 43 // DestinationNS, DestinationName is the same, but for the destination 44 // service. The same rules apply. The destination is always a Consul 45 // service. 46 SourceNS, SourceName string 47 DestinationNS, DestinationName string 48 49 // SourceType is the type of the value for the source. 50 SourceType IntentionSourceType 51 52 // Action is whether this is a whitelist or blacklist intention. 53 Action IntentionAction 54 55 // DefaultAddr, DefaultPort of the local listening proxy (if any) to 56 // make this connection. 57 DefaultAddr string 58 DefaultPort int 59 60 // Meta is arbitrary metadata associated with the intention. This is 61 // opaque to Consul but is served in API responses. 62 Meta map[string]string 63 64 // Precedence is the order that the intention will be applied, with 65 // larger numbers being applied first. This is a read-only field, on 66 // any intention update it is updated. 67 Precedence int 68 69 // CreatedAt and UpdatedAt keep track of when this record was created 70 // or modified. 71 CreatedAt, UpdatedAt time.Time `mapstructure:"-"` 72 73 RaftIndex 74 } 75 76 // Validate returns an error if the intention is invalid for inserting 77 // or updating. 78 func (x *Intention) Validate() error { 79 var result error 80 81 // Empty values 82 if x.SourceNS == "" { 83 result = multierror.Append(result, fmt.Errorf("SourceNS must be set")) 84 } 85 if x.SourceName == "" { 86 result = multierror.Append(result, fmt.Errorf("SourceName must be set")) 87 } 88 if x.DestinationNS == "" { 89 result = multierror.Append(result, fmt.Errorf("DestinationNS must be set")) 90 } 91 if x.DestinationName == "" { 92 result = multierror.Append(result, fmt.Errorf("DestinationName must be set")) 93 } 94 95 // Wildcard usage verification 96 if x.SourceNS != IntentionWildcard { 97 if strings.Contains(x.SourceNS, IntentionWildcard) { 98 result = multierror.Append(result, fmt.Errorf( 99 "SourceNS: wildcard character '*' cannot be used with partial values")) 100 } 101 } 102 if x.SourceName != IntentionWildcard { 103 if strings.Contains(x.SourceName, IntentionWildcard) { 104 result = multierror.Append(result, fmt.Errorf( 105 "SourceName: wildcard character '*' cannot be used with partial values")) 106 } 107 108 if x.SourceNS == IntentionWildcard { 109 result = multierror.Append(result, fmt.Errorf( 110 "SourceName: exact value cannot follow wildcard namespace")) 111 } 112 } 113 if x.DestinationNS != IntentionWildcard { 114 if strings.Contains(x.DestinationNS, IntentionWildcard) { 115 result = multierror.Append(result, fmt.Errorf( 116 "DestinationNS: wildcard character '*' cannot be used with partial values")) 117 } 118 } 119 if x.DestinationName != IntentionWildcard { 120 if strings.Contains(x.DestinationName, IntentionWildcard) { 121 result = multierror.Append(result, fmt.Errorf( 122 "DestinationName: wildcard character '*' cannot be used with partial values")) 123 } 124 125 if x.DestinationNS == IntentionWildcard { 126 result = multierror.Append(result, fmt.Errorf( 127 "DestinationName: exact value cannot follow wildcard namespace")) 128 } 129 } 130 131 // Length of opaque values 132 if len(x.Description) > metaValueMaxLength { 133 result = multierror.Append(result, fmt.Errorf( 134 "Description exceeds maximum length %d", metaValueMaxLength)) 135 } 136 if len(x.Meta) > metaMaxKeyPairs { 137 result = multierror.Append(result, fmt.Errorf( 138 "Meta exceeds maximum element count %d", metaMaxKeyPairs)) 139 } 140 for k, v := range x.Meta { 141 if len(k) > metaKeyMaxLength { 142 result = multierror.Append(result, fmt.Errorf( 143 "Meta key %q exceeds maximum length %d", k, metaKeyMaxLength)) 144 } 145 if len(v) > metaValueMaxLength { 146 result = multierror.Append(result, fmt.Errorf( 147 "Meta value for key %q exceeds maximum length %d", k, metaValueMaxLength)) 148 } 149 } 150 151 switch x.Action { 152 case IntentionActionAllow, IntentionActionDeny: 153 default: 154 result = multierror.Append(result, fmt.Errorf( 155 "Action must be set to 'allow' or 'deny'")) 156 } 157 158 switch x.SourceType { 159 case IntentionSourceConsul: 160 default: 161 result = multierror.Append(result, fmt.Errorf( 162 "SourceType must be set to 'consul'")) 163 } 164 165 return result 166 } 167 168 // UpdatePrecedence sets the Precedence value based on the fields of this 169 // structure. 170 func (x *Intention) UpdatePrecedence() { 171 // Max maintains the maximum value that the precedence can be depending 172 // on the number of exact values in the destination. 173 var max int 174 switch x.countExact(x.DestinationNS, x.DestinationName) { 175 case 2: 176 max = 9 177 case 1: 178 max = 6 179 case 0: 180 max = 3 181 default: 182 // This shouldn't be possible, just set it to zero 183 x.Precedence = 0 184 return 185 } 186 187 // Given the maximum, the exact value is determined based on the 188 // number of source exact values. 189 countSrc := x.countExact(x.SourceNS, x.SourceName) 190 x.Precedence = max - (2 - countSrc) 191 } 192 193 // countExact counts the number of exact values (not wildcards) in 194 // the given namespace and name. 195 func (x *Intention) countExact(ns, n string) int { 196 // If NS is wildcard, it must be zero since wildcards only follow exact 197 if ns == IntentionWildcard { 198 return 0 199 } 200 201 // Same reasoning as above, a wildcard can only follow an exact value 202 // and an exact value cannot follow a wildcard, so if name is a wildcard 203 // we must have exactly one. 204 if n == IntentionWildcard { 205 return 1 206 } 207 208 return 2 209 } 210 211 // GetACLPrefix returns the prefix to look up the ACL policy for this 212 // intention, and a boolean noting whether the prefix is valid to check 213 // or not. You must check the ok value before using the prefix. 214 func (x *Intention) GetACLPrefix() (string, bool) { 215 return x.DestinationName, x.DestinationName != "" 216 } 217 218 // String returns a human-friendly string for this intention. 219 func (x *Intention) String() string { 220 return fmt.Sprintf("%s %s/%s => %s/%s (ID: %s, Precedence: %d)", 221 strings.ToUpper(string(x.Action)), 222 x.SourceNS, x.SourceName, 223 x.DestinationNS, x.DestinationName, 224 x.ID, x.Precedence) 225 } 226 227 // EstimateSize returns an estimate (in bytes) of the size of this structure when encoded. 228 func (x *Intention) EstimateSize() int { 229 // 60 = 36 (uuid) + 16 (RaftIndex) + 4 (Precedence) + 4 (DefaultPort) 230 size := 60 + len(x.Description) + len(x.SourceNS) + len(x.SourceName) + len(x.DestinationNS) + 231 len(x.DestinationName) + len(x.SourceType) + len(x.Action) + len(x.DefaultAddr) 232 233 for k, v := range x.Meta { 234 size += len(k) + len(v) 235 } 236 237 return size 238 } 239 240 // IntentionAction is the action that the intention represents. This 241 // can be "allow" or "deny" to whitelist or blacklist intentions. 242 type IntentionAction string 243 244 const ( 245 IntentionActionAllow IntentionAction = "allow" 246 IntentionActionDeny IntentionAction = "deny" 247 ) 248 249 // IntentionSourceType is the type of the source within an intention. 250 type IntentionSourceType string 251 252 const ( 253 // IntentionSourceConsul is a service within the Consul catalog. 254 IntentionSourceConsul IntentionSourceType = "consul" 255 ) 256 257 // Intentions is a list of intentions. 258 type Intentions []*Intention 259 260 // IndexedIntentions represents a list of intentions for RPC responses. 261 type IndexedIntentions struct { 262 Intentions Intentions 263 QueryMeta 264 } 265 266 // IndexedIntentionMatches represents the list of matches for a match query. 267 type IndexedIntentionMatches struct { 268 Matches []Intentions 269 QueryMeta 270 } 271 272 // IntentionOp is the operation for a request related to intentions. 273 type IntentionOp string 274 275 const ( 276 IntentionOpCreate IntentionOp = "create" 277 IntentionOpUpdate IntentionOp = "update" 278 IntentionOpDelete IntentionOp = "delete" 279 ) 280 281 // IntentionRequest is used to create, update, and delete intentions. 282 type IntentionRequest struct { 283 // Datacenter is the target for this request. 284 Datacenter string 285 286 // Op is the type of operation being requested. 287 Op IntentionOp 288 289 // Intention is the intention. 290 Intention *Intention 291 292 // WriteRequest is a common struct containing ACL tokens and other 293 // write-related common elements for requests. 294 WriteRequest 295 } 296 297 // RequestDatacenter returns the datacenter for a given request. 298 func (q *IntentionRequest) RequestDatacenter() string { 299 return q.Datacenter 300 } 301 302 // IntentionMatchType is the target for a match request. For example, 303 // matching by source will look for all intentions that match the given 304 // source value. 305 type IntentionMatchType string 306 307 const ( 308 IntentionMatchSource IntentionMatchType = "source" 309 IntentionMatchDestination IntentionMatchType = "destination" 310 ) 311 312 // IntentionQueryRequest is used to query intentions. 313 type IntentionQueryRequest struct { 314 // Datacenter is the target this request is intended for. 315 Datacenter string 316 317 // IntentionID is the ID of a specific intention. 318 IntentionID string 319 320 // Match is non-nil if we're performing a match query. A match will 321 // find intentions that "match" the given parameters. A match includes 322 // resolving wildcards. 323 Match *IntentionQueryMatch 324 325 // Check is non-nil if we're performing a test query. A test will 326 // return allowed/deny based on an exact match. 327 Check *IntentionQueryCheck 328 329 // Options for queries 330 QueryOptions 331 } 332 333 // RequestDatacenter returns the datacenter for a given request. 334 func (q *IntentionQueryRequest) RequestDatacenter() string { 335 return q.Datacenter 336 } 337 338 // CacheInfo implements cache.Request 339 func (q *IntentionQueryRequest) CacheInfo() cache.RequestInfo { 340 // We only support caching Match queries, so if Match isn't set, 341 // then return an empty info object which will cause a pass-through 342 // (and likely fail). 343 if q.Match == nil { 344 return cache.RequestInfo{} 345 } 346 347 info := cache.RequestInfo{ 348 Token: q.Token, 349 Datacenter: q.Datacenter, 350 MinIndex: q.MinQueryIndex, 351 Timeout: q.MaxQueryTime, 352 } 353 354 // Calculate the cache key via just hashing the Match struct. This 355 // has been configured so things like ordering of entries has no 356 // effect (via struct tags). 357 v, err := hashstructure.Hash(q.Match, nil) 358 if err == nil { 359 // If there is an error, we don't set the key. A blank key forces 360 // no cache for this request so the request is forwarded directly 361 // to the server. 362 info.Key = strconv.FormatUint(v, 16) 363 } 364 365 return info 366 } 367 368 // IntentionQueryMatch are the parameters for performing a match request 369 // against the state store. 370 type IntentionQueryMatch struct { 371 Type IntentionMatchType 372 Entries []IntentionMatchEntry 373 } 374 375 // IntentionMatchEntry is a single entry for matching an intention. 376 type IntentionMatchEntry struct { 377 Namespace string 378 Name string 379 } 380 381 // IntentionQueryCheck are the parameters for performing a test request. 382 type IntentionQueryCheck struct { 383 // SourceNS, SourceName, DestinationNS, and DestinationName are the 384 // source and namespace, respectively, for the test. These must be 385 // exact values. 386 SourceNS, SourceName string 387 DestinationNS, DestinationName string 388 389 // SourceType is the type of the value for the source. 390 SourceType IntentionSourceType 391 } 392 393 // GetACLPrefix returns the prefix to look up the ACL policy for this 394 // request, and a boolean noting whether the prefix is valid to check 395 // or not. You must check the ok value before using the prefix. 396 func (q *IntentionQueryCheck) GetACLPrefix() (string, bool) { 397 return q.DestinationName, q.DestinationName != "" 398 } 399 400 // IntentionQueryCheckResponse is the response for a test request. 401 type IntentionQueryCheckResponse struct { 402 Allowed bool 403 } 404 405 // IntentionPrecedenceSorter takes a list of intentions and sorts them 406 // based on the match precedence rules for intentions. The intentions 407 // closer to the head of the list have higher precedence. i.e. index 0 has 408 // the highest precedence. 409 type IntentionPrecedenceSorter Intentions 410 411 func (s IntentionPrecedenceSorter) Len() int { return len(s) } 412 func (s IntentionPrecedenceSorter) Swap(i, j int) { 413 s[i], s[j] = s[j], s[i] 414 } 415 416 func (s IntentionPrecedenceSorter) Less(i, j int) bool { 417 a, b := s[i], s[j] 418 if a.Precedence != b.Precedence { 419 return a.Precedence > b.Precedence 420 } 421 422 // Tie break on lexicographic order of the 4-tuple in canonical form (SrcNS, 423 // Src, DstNS, Dst). This is arbitrary but it keeps sorting deterministic 424 // which is a nice property for consistency. It is arguably open to abuse if 425 // implementations rely on this however by definition the order among 426 // same-precedence rules is arbitrary and doesn't affect whether an allow or 427 // deny rule is acted on since all applicable rules are checked. 428 if a.SourceNS != b.SourceNS { 429 return a.SourceNS < b.SourceNS 430 } 431 if a.SourceName != b.SourceName { 432 return a.SourceName < b.SourceName 433 } 434 if a.DestinationNS != b.DestinationNS { 435 return a.DestinationNS < b.DestinationNS 436 } 437 return a.DestinationName < b.DestinationName 438 }