1package state 2 3import ( 4 "errors" 5 "fmt" 6 "github.com/hashicorp/consul/agent/connect" 7 "sort" 8 9 "github.com/hashicorp/go-memdb" 10 11 "github.com/hashicorp/consul/acl" 12 "github.com/hashicorp/consul/agent/structs" 13) 14 15const tableConnectIntentions = "connect-intentions" 16 17// intentionsTableSchema returns a new table schema used for storing 18// intentions for Connect. 19func intentionsTableSchema() *memdb.TableSchema { 20 return &memdb.TableSchema{ 21 Name: tableConnectIntentions, 22 Indexes: map[string]*memdb.IndexSchema{ 23 indexID: { 24 Name: indexID, 25 AllowMissing: false, 26 Unique: true, 27 Indexer: &memdb.UUIDFieldIndex{ 28 Field: "ID", 29 }, 30 }, 31 "destination": { 32 Name: "destination", 33 AllowMissing: true, 34 // This index is not unique since we need uniqueness across the whole 35 // 4-tuple. 36 Unique: false, 37 Indexer: &memdb.CompoundIndex{ 38 Indexes: []memdb.Indexer{ 39 &memdb.StringFieldIndex{ 40 Field: "DestinationNS", 41 Lowercase: true, 42 }, 43 &memdb.StringFieldIndex{ 44 Field: "DestinationName", 45 Lowercase: true, 46 }, 47 }, 48 }, 49 }, 50 "source": { 51 Name: "source", 52 AllowMissing: true, 53 // This index is not unique since we need uniqueness across the whole 54 // 4-tuple. 55 Unique: false, 56 Indexer: &memdb.CompoundIndex{ 57 Indexes: []memdb.Indexer{ 58 &memdb.StringFieldIndex{ 59 Field: "SourceNS", 60 Lowercase: true, 61 }, 62 &memdb.StringFieldIndex{ 63 Field: "SourceName", 64 Lowercase: true, 65 }, 66 }, 67 }, 68 }, 69 "source_destination": { 70 Name: "source_destination", 71 AllowMissing: true, 72 Unique: true, 73 Indexer: &memdb.CompoundIndex{ 74 Indexes: []memdb.Indexer{ 75 &memdb.StringFieldIndex{ 76 Field: "SourceNS", 77 Lowercase: true, 78 }, 79 &memdb.StringFieldIndex{ 80 Field: "SourceName", 81 Lowercase: true, 82 }, 83 &memdb.StringFieldIndex{ 84 Field: "DestinationNS", 85 Lowercase: true, 86 }, 87 &memdb.StringFieldIndex{ 88 Field: "DestinationName", 89 Lowercase: true, 90 }, 91 }, 92 }, 93 }, 94 }, 95 } 96} 97 98// LegacyIntentions is used to pull all the intentions from the snapshot. 99// 100// Deprecated: service-intentions config entries are handled as config entries 101// in the snapshot. 102func (s *Snapshot) LegacyIntentions() (structs.Intentions, error) { 103 ixns, err := s.tx.Get(tableConnectIntentions, "id") 104 if err != nil { 105 return nil, err 106 } 107 108 var ret structs.Intentions 109 for wrapped := ixns.Next(); wrapped != nil; wrapped = ixns.Next() { 110 ret = append(ret, wrapped.(*structs.Intention)) 111 } 112 113 return ret, nil 114} 115 116// LegacyIntention is used when restoring from a snapshot. 117// 118// Deprecated: service-intentions config entries are handled as config entries 119// in the snapshot. 120func (s *Restore) LegacyIntention(ixn *structs.Intention) error { 121 // Insert the intention 122 if err := s.tx.Insert(tableConnectIntentions, ixn); err != nil { 123 return fmt.Errorf("failed restoring intention: %s", err) 124 } 125 if err := indexUpdateMaxTxn(s.tx, ixn.ModifyIndex, tableConnectIntentions); err != nil { 126 return fmt.Errorf("failed updating index: %s", err) 127 } 128 129 return nil 130} 131 132// AreIntentionsInConfigEntries determines which table is the canonical store 133// for intentions data. 134func (s *Store) AreIntentionsInConfigEntries() (bool, error) { 135 tx := s.db.Txn(false) 136 defer tx.Abort() 137 return areIntentionsInConfigEntries(tx, nil) 138} 139 140func areIntentionsInConfigEntries(tx ReadTxn, ws memdb.WatchSet) (bool, error) { 141 _, entry, err := systemMetadataGetTxn(tx, ws, structs.SystemMetadataIntentionFormatKey) 142 if err != nil { 143 return false, fmt.Errorf("failed system metadatalookup: %s", err) 144 } 145 if entry == nil { 146 return false, nil 147 } 148 return entry.Value == structs.SystemMetadataIntentionFormatConfigValue, nil 149} 150 151// LegacyIntentions is like Intentions() but only returns legacy intentions. 152// This is exposed for migration purposes. 153func (s *Store) LegacyIntentions(ws memdb.WatchSet, entMeta *structs.EnterpriseMeta) (uint64, structs.Intentions, error) { 154 tx := s.db.Txn(false) 155 defer tx.Abort() 156 157 idx, results, _, err := legacyIntentionsListTxn(tx, ws, entMeta) 158 return idx, results, err 159} 160 161// Intentions returns the list of all intentions. The boolean response value is true if it came from config entries. 162func (s *Store) Intentions(ws memdb.WatchSet, entMeta *structs.EnterpriseMeta) (uint64, structs.Intentions, bool, error) { 163 tx := s.db.Txn(false) 164 defer tx.Abort() 165 166 usingConfigEntries, err := areIntentionsInConfigEntries(tx, ws) 167 if err != nil { 168 return 0, nil, false, err 169 } 170 if !usingConfigEntries { 171 return legacyIntentionsListTxn(tx, ws, entMeta) 172 } 173 return configIntentionsListTxn(tx, ws, entMeta) 174} 175 176func legacyIntentionsListTxn(tx ReadTxn, ws memdb.WatchSet, entMeta *structs.EnterpriseMeta) (uint64, structs.Intentions, bool, error) { 177 // Get the index 178 idx := maxIndexTxn(tx, tableConnectIntentions) 179 if idx < 1 { 180 idx = 1 181 } 182 183 iter, err := intentionListTxn(tx, entMeta) 184 if err != nil { 185 return 0, nil, false, fmt.Errorf("failed intention lookup: %s", err) 186 } 187 188 ws.Add(iter.WatchCh()) 189 190 var results structs.Intentions 191 for ixn := iter.Next(); ixn != nil; ixn = iter.Next() { 192 results = append(results, ixn.(*structs.Intention)) 193 } 194 195 // Sort by precedence just because that's nicer and probably what most clients 196 // want for presentation. 197 sort.Sort(structs.IntentionPrecedenceSorter(results)) 198 199 return idx, results, false, nil 200} 201 202var ErrLegacyIntentionsAreDisabled = errors.New("Legacy intention modifications are disabled after the config entry migration.") 203 204func (s *Store) IntentionMutation(idx uint64, op structs.IntentionOp, mut *structs.IntentionMutation) error { 205 tx := s.db.WriteTxn(idx) 206 defer tx.Abort() 207 208 usingConfigEntries, err := areIntentionsInConfigEntries(tx, nil) 209 if err != nil { 210 return err 211 } 212 if !usingConfigEntries { 213 return errors.New("state: IntentionMutation() is not allowed when intentions are not stored in config entries") 214 } 215 216 switch op { 217 case structs.IntentionOpCreate: 218 if err := s.intentionMutationLegacyCreate(tx, idx, mut.Destination, mut.Value); err != nil { 219 return err 220 } 221 case structs.IntentionOpUpdate: 222 if err := s.intentionMutationLegacyUpdate(tx, idx, mut.ID, mut.Value); err != nil { 223 return err 224 } 225 case structs.IntentionOpDelete: 226 if mut.ID == "" { 227 if err := s.intentionMutationDelete(tx, idx, mut.Destination, mut.Source); err != nil { 228 return err 229 } 230 } else { 231 if err := s.intentionMutationLegacyDelete(tx, idx, mut.ID); err != nil { 232 return err 233 } 234 } 235 case structs.IntentionOpUpsert: 236 if err := s.intentionMutationUpsert(tx, idx, mut.Destination, mut.Source, mut.Value); err != nil { 237 return err 238 } 239 case structs.IntentionOpDeleteAll: 240 // This is an internal operation initiated by the leader and is not 241 // exposed for general RPC use. 242 return fmt.Errorf("Invalid Intention mutation operation '%s'", op) 243 default: 244 return fmt.Errorf("Invalid Intention mutation operation '%s'", op) 245 } 246 247 return tx.Commit() 248} 249 250func (s *Store) intentionMutationLegacyCreate( 251 tx WriteTxn, 252 idx uint64, 253 dest structs.ServiceName, 254 value *structs.SourceIntention, 255) error { 256 _, configEntry, err := configEntryTxn(tx, nil, structs.ServiceIntentions, dest.Name, &dest.EnterpriseMeta) 257 if err != nil { 258 return fmt.Errorf("service-intentions config entry lookup failed: %v", err) 259 } 260 261 var upsertEntry *structs.ServiceIntentionsConfigEntry 262 if configEntry == nil { 263 upsertEntry = &structs.ServiceIntentionsConfigEntry{ 264 Kind: structs.ServiceIntentions, 265 Name: dest.Name, 266 EnterpriseMeta: dest.EnterpriseMeta, 267 Sources: []*structs.SourceIntention{value}, 268 } 269 } else { 270 prevEntry := configEntry.(*structs.ServiceIntentionsConfigEntry) 271 272 if err := checkLegacyIntentionApplyAllowed(prevEntry); err != nil { 273 return err 274 } 275 276 upsertEntry = prevEntry.Clone() 277 upsertEntry.Sources = append(upsertEntry.Sources, value) 278 } 279 280 if err := upsertEntry.LegacyNormalize(); err != nil { 281 return err 282 } 283 if err := upsertEntry.LegacyValidate(); err != nil { 284 return err 285 } 286 287 if err := ensureConfigEntryTxn(tx, idx, upsertEntry); err != nil { 288 return err 289 } 290 291 return nil 292} 293 294func (s *Store) intentionMutationLegacyUpdate( 295 tx WriteTxn, 296 idx uint64, 297 legacyID string, 298 value *structs.SourceIntention, 299) error { 300 // This variant is just for legacy UUID-based intentions. 301 302 _, prevEntry, ixn, err := s.IntentionGet(nil, legacyID) 303 if err != nil { 304 return fmt.Errorf("Intention lookup failed: %v", err) 305 } 306 if ixn == nil || prevEntry == nil { 307 return fmt.Errorf("Cannot modify non-existent intention: '%s'", legacyID) 308 } 309 310 if err := checkLegacyIntentionApplyAllowed(prevEntry); err != nil { 311 return err 312 } 313 314 upsertEntry := prevEntry.Clone() 315 316 foundMatch := upsertEntry.UpdateSourceByLegacyID( 317 legacyID, 318 value, 319 ) 320 if !foundMatch { 321 return fmt.Errorf("Cannot modify non-existent intention: '%s'", legacyID) 322 } 323 324 if err := upsertEntry.LegacyNormalize(); err != nil { 325 return err 326 } 327 if err := upsertEntry.LegacyValidate(); err != nil { 328 return err 329 } 330 331 if err := ensureConfigEntryTxn(tx, idx, upsertEntry); err != nil { 332 return err 333 } 334 335 return nil 336} 337 338func (s *Store) intentionMutationDelete( 339 tx WriteTxn, 340 idx uint64, 341 dest structs.ServiceName, 342 src structs.ServiceName, 343) error { 344 _, configEntry, err := configEntryTxn(tx, nil, structs.ServiceIntentions, dest.Name, &dest.EnterpriseMeta) 345 if err != nil { 346 return fmt.Errorf("service-intentions config entry lookup failed: %v", err) 347 } 348 if configEntry == nil { 349 return nil 350 } 351 352 prevEntry := configEntry.(*structs.ServiceIntentionsConfigEntry) 353 upsertEntry := prevEntry.Clone() 354 355 deleted := upsertEntry.DeleteSourceByName(src) 356 if !deleted { 357 return nil 358 } 359 360 if upsertEntry == nil || len(upsertEntry.Sources) == 0 { 361 return deleteConfigEntryTxn( 362 tx, 363 idx, 364 structs.ServiceIntentions, 365 dest.Name, 366 &dest.EnterpriseMeta, 367 ) 368 } 369 370 if err := upsertEntry.Normalize(); err != nil { 371 return err 372 } 373 if err := upsertEntry.Validate(); err != nil { 374 return err 375 } 376 377 if err := ensureConfigEntryTxn(tx, idx, upsertEntry); err != nil { 378 return err 379 } 380 381 return nil 382} 383 384func (s *Store) intentionMutationLegacyDelete( 385 tx WriteTxn, 386 idx uint64, 387 legacyID string, 388) error { 389 _, prevEntry, ixn, err := s.IntentionGet(nil, legacyID) 390 if err != nil { 391 return fmt.Errorf("Intention lookup failed: %v", err) 392 } 393 if ixn == nil || prevEntry == nil { 394 return fmt.Errorf("Cannot delete non-existent intention: '%s'", legacyID) 395 } 396 397 if err := checkLegacyIntentionApplyAllowed(prevEntry); err != nil { 398 return err 399 } 400 401 upsertEntry := prevEntry.Clone() 402 403 deleted := upsertEntry.DeleteSourceByLegacyID(legacyID) 404 if !deleted { 405 return fmt.Errorf("Cannot delete non-existent intention: '%s'", legacyID) 406 } 407 408 if upsertEntry == nil || len(upsertEntry.Sources) == 0 { 409 return deleteConfigEntryTxn( 410 tx, 411 idx, 412 structs.ServiceIntentions, 413 prevEntry.Name, 414 &prevEntry.EnterpriseMeta, 415 ) 416 } 417 418 if err := upsertEntry.LegacyNormalize(); err != nil { 419 return err 420 } 421 if err := upsertEntry.LegacyValidate(); err != nil { 422 return err 423 } 424 425 if err := ensureConfigEntryTxn(tx, idx, upsertEntry); err != nil { 426 return err 427 } 428 429 return nil 430} 431 432func (s *Store) intentionMutationUpsert( 433 tx WriteTxn, 434 idx uint64, 435 dest structs.ServiceName, 436 src structs.ServiceName, 437 value *structs.SourceIntention, 438) error { 439 // This variant is just for config-entry based intentions. 440 441 _, configEntry, err := configEntryTxn(tx, nil, structs.ServiceIntentions, dest.Name, &dest.EnterpriseMeta) 442 if err != nil { 443 return fmt.Errorf("service-intentions config entry lookup failed: %v", err) 444 } 445 446 var prevEntry *structs.ServiceIntentionsConfigEntry 447 if configEntry != nil { 448 prevEntry = configEntry.(*structs.ServiceIntentionsConfigEntry) 449 } 450 451 var upsertEntry *structs.ServiceIntentionsConfigEntry 452 453 if prevEntry == nil { 454 upsertEntry = &structs.ServiceIntentionsConfigEntry{ 455 Kind: structs.ServiceIntentions, 456 Name: dest.Name, 457 EnterpriseMeta: dest.EnterpriseMeta, 458 Sources: []*structs.SourceIntention{value}, 459 } 460 } else { 461 upsertEntry = prevEntry.Clone() 462 463 upsertEntry.UpsertSourceByName(src, value) 464 } 465 466 if err := upsertEntry.Normalize(); err != nil { 467 return err 468 } 469 if err := upsertEntry.Validate(); err != nil { 470 return err 471 } 472 473 if err := ensureConfigEntryTxn(tx, idx, upsertEntry); err != nil { 474 return err 475 } 476 477 return nil 478} 479 480func checkLegacyIntentionApplyAllowed(prevEntry *structs.ServiceIntentionsConfigEntry) error { 481 if prevEntry == nil { 482 return nil 483 } 484 if prevEntry.LegacyIDFieldsAreAllSet() { 485 return nil 486 } 487 488 sn := prevEntry.DestinationServiceName() 489 return fmt.Errorf("cannot use legacy intention API to edit intentions with a destination of %q after editing them via a service-intentions config entry", sn.String()) 490} 491 492// LegacyIntentionSet creates or updates an intention. 493// 494// Deprecated: Edit service-intentions config entries directly. 495func (s *Store) LegacyIntentionSet(idx uint64, ixn *structs.Intention) error { 496 tx := s.db.WriteTxn(idx) 497 defer tx.Abort() 498 499 usingConfigEntries, err := areIntentionsInConfigEntries(tx, nil) 500 if err != nil { 501 return err 502 } 503 if usingConfigEntries { 504 return ErrLegacyIntentionsAreDisabled 505 } 506 507 if err := legacyIntentionSetTxn(tx, idx, ixn); err != nil { 508 return err 509 } 510 511 return tx.Commit() 512} 513 514// legacyIntentionSetTxn is the inner method used to insert an intention with 515// the proper indexes into the state store. 516func legacyIntentionSetTxn(tx WriteTxn, idx uint64, ixn *structs.Intention) error { 517 // ID is required 518 if ixn.ID == "" { 519 return ErrMissingIntentionID 520 } 521 522 // Ensure Precedence is populated correctly on "write" 523 //nolint:staticcheck 524 ixn.UpdatePrecedence() 525 526 // Check for an existing intention 527 existing, err := tx.First(tableConnectIntentions, "id", ixn.ID) 528 if err != nil { 529 return fmt.Errorf("failed intention lookup: %s", err) 530 } 531 if existing != nil { 532 oldIxn := existing.(*structs.Intention) 533 ixn.CreateIndex = oldIxn.CreateIndex 534 ixn.CreatedAt = oldIxn.CreatedAt 535 } else { 536 ixn.CreateIndex = idx 537 } 538 ixn.ModifyIndex = idx 539 540 // Check for duplicates on the 4-tuple. 541 duplicate, err := tx.First(tableConnectIntentions, "source_destination", 542 ixn.SourceNS, ixn.SourceName, ixn.DestinationNS, ixn.DestinationName) 543 if err != nil { 544 return fmt.Errorf("failed intention lookup: %s", err) 545 } 546 if duplicate != nil { 547 dupIxn := duplicate.(*structs.Intention) 548 // Same ID is OK - this is an update 549 if dupIxn.ID != ixn.ID { 550 return fmt.Errorf("duplicate intention found: %s", dupIxn.String()) 551 } 552 } 553 554 // We always force meta to be non-nil so that we its an empty map. 555 // This makes it easy for API responses to not nil-check this everywhere. 556 if ixn.Meta == nil { 557 ixn.Meta = make(map[string]string) 558 } 559 560 // Insert 561 if err := tx.Insert(tableConnectIntentions, ixn); err != nil { 562 return err 563 } 564 if err := tx.Insert(tableIndex, &IndexEntry{tableConnectIntentions, idx}); err != nil { 565 return fmt.Errorf("failed updating index: %s", err) 566 } 567 568 return nil 569} 570 571// IntentionGet returns the given intention by ID. 572func (s *Store) IntentionGet(ws memdb.WatchSet, id string) (uint64, *structs.ServiceIntentionsConfigEntry, *structs.Intention, error) { 573 tx := s.db.Txn(false) 574 defer tx.Abort() 575 576 usingConfigEntries, err := areIntentionsInConfigEntries(tx, ws) 577 if err != nil { 578 return 0, nil, nil, err 579 } 580 if !usingConfigEntries { 581 idx, ixn, err := legacyIntentionGetTxn(tx, ws, id) 582 return idx, nil, ixn, err 583 } 584 return configIntentionGetTxn(tx, ws, id) 585} 586 587func legacyIntentionGetTxn(tx ReadTxn, ws memdb.WatchSet, id string) (uint64, *structs.Intention, error) { 588 // Get the table index. 589 idx := maxIndexTxn(tx, tableConnectIntentions) 590 if idx < 1 { 591 idx = 1 592 } 593 594 // Look up by its ID. 595 watchCh, intention, err := tx.FirstWatch(tableConnectIntentions, "id", id) 596 if err != nil { 597 return 0, nil, fmt.Errorf("failed intention lookup: %s", err) 598 } 599 ws.Add(watchCh) 600 601 // Convert the interface{} if it is non-nil 602 var result *structs.Intention 603 if intention != nil { 604 result = intention.(*structs.Intention) 605 } 606 607 return idx, result, nil 608} 609 610// IntentionGetExact returns the given intention by it's full unique name. 611func (s *Store) IntentionGetExact(ws memdb.WatchSet, args *structs.IntentionQueryExact) (uint64, *structs.ServiceIntentionsConfigEntry, *structs.Intention, error) { 612 tx := s.db.Txn(false) 613 defer tx.Abort() 614 615 usingConfigEntries, err := areIntentionsInConfigEntries(tx, ws) 616 if err != nil { 617 return 0, nil, nil, err 618 } 619 if !usingConfigEntries { 620 idx, ixn, err := s.legacyIntentionGetExactTxn(tx, ws, args) 621 return idx, nil, ixn, err 622 } 623 return s.configIntentionGetExactTxn(tx, ws, args) 624} 625 626func (s *Store) legacyIntentionGetExactTxn(tx ReadTxn, ws memdb.WatchSet, args *structs.IntentionQueryExact) (uint64, *structs.Intention, error) { 627 if err := args.Validate(); err != nil { 628 return 0, nil, err 629 } 630 631 // Get the table index. 632 idx := maxIndexTxn(tx, tableConnectIntentions) 633 if idx < 1 { 634 idx = 1 635 } 636 637 // Look up by its full name. 638 watchCh, intention, err := tx.FirstWatch(tableConnectIntentions, "source_destination", 639 args.SourceNS, args.SourceName, args.DestinationNS, args.DestinationName) 640 if err != nil { 641 return 0, nil, fmt.Errorf("failed intention lookup: %s", err) 642 } 643 ws.Add(watchCh) 644 645 // Convert the interface{} if it is non-nil 646 var result *structs.Intention 647 if intention != nil { 648 result = intention.(*structs.Intention) 649 } 650 651 return idx, result, nil 652} 653 654// LegacyIntentionDelete deletes the given intention by ID. 655// 656// Deprecated: Edit service-intentions config entries directly. 657func (s *Store) LegacyIntentionDelete(idx uint64, id string) error { 658 tx := s.db.WriteTxn(idx) 659 defer tx.Abort() 660 661 usingConfigEntries, err := areIntentionsInConfigEntries(tx, nil) 662 if err != nil { 663 return err 664 } 665 if usingConfigEntries { 666 return ErrLegacyIntentionsAreDisabled 667 } 668 669 if err := legacyIntentionDeleteTxn(tx, idx, id); err != nil { 670 return fmt.Errorf("failed intention delete: %s", err) 671 } 672 673 return tx.Commit() 674} 675 676// legacyIntentionDeleteTxn is the inner method used to delete a legacy intention 677// with the proper indexes into the state store. 678func legacyIntentionDeleteTxn(tx WriteTxn, idx uint64, queryID string) error { 679 // Pull the query. 680 wrapped, err := tx.First(tableConnectIntentions, "id", queryID) 681 if err != nil { 682 return fmt.Errorf("failed intention lookup: %s", err) 683 } 684 if wrapped == nil { 685 return nil 686 } 687 688 // Delete the query and update the index. 689 if err := tx.Delete(tableConnectIntentions, wrapped); err != nil { 690 return fmt.Errorf("failed intention delete: %s", err) 691 } 692 if err := tx.Insert(tableIndex, &IndexEntry{tableConnectIntentions, idx}); err != nil { 693 return fmt.Errorf("failed updating index: %s", err) 694 } 695 696 return nil 697} 698 699// LegacyIntentionDeleteAll deletes all legacy intentions. This is part of the 700// config entry migration code. 701func (s *Store) LegacyIntentionDeleteAll(idx uint64) error { 702 tx := s.db.WriteTxn(idx) 703 defer tx.Abort() 704 705 // Delete the table and update the index. 706 if _, err := tx.DeleteAll(tableConnectIntentions, "id"); err != nil { 707 return fmt.Errorf("failed intention delete-all: %s", err) 708 } 709 if err := tx.Insert(tableIndex, &IndexEntry{tableConnectIntentions, idx}); err != nil { 710 return fmt.Errorf("failed updating index: %s", err) 711 } 712 // Also bump the index for the config entry table so that 713 // secondaries can correctly know when they've replicated all of the service-intentions 714 // config entries that USED to exist in the old intentions table. 715 if err := tx.Insert(tableIndex, &IndexEntry{tableConfigEntries, idx}); err != nil { 716 return fmt.Errorf("failed updating index: %s", err) 717 } 718 719 // Also set a system metadata flag indicating the transition has occurred. 720 metadataEntry := &structs.SystemMetadataEntry{ 721 Key: structs.SystemMetadataIntentionFormatKey, 722 Value: structs.SystemMetadataIntentionFormatConfigValue, 723 RaftIndex: structs.RaftIndex{ 724 CreateIndex: idx, 725 ModifyIndex: idx, 726 }, 727 } 728 if err := systemMetadataSetTxn(tx, idx, metadataEntry); err != nil { 729 return fmt.Errorf("failed updating system metadata key %q: %s", metadataEntry.Key, err) 730 } 731 732 return tx.Commit() 733} 734 735// IntentionDecision returns whether a connection should be allowed to a source or destination given a set of intentions. 736// 737// allowPermissions determines whether the presence of L7 permissions leads to a DENY decision. 738// This should be false when evaluating a connection between a source and destination, but not the request that will be sent. 739func (s *Store) IntentionDecision( 740 target, targetNS string, intentions structs.Intentions, matchType structs.IntentionMatchType, 741 defaultDecision acl.EnforcementDecision, allowPermissions bool, 742) (structs.IntentionDecisionSummary, error) { 743 744 // Figure out which source matches this request. 745 var ixnMatch *structs.Intention 746 for _, ixn := range intentions { 747 if _, ok := connect.AuthorizeIntentionTarget(target, targetNS, ixn, matchType); ok { 748 ixnMatch = ixn 749 break 750 } 751 } 752 753 resp := structs.IntentionDecisionSummary{ 754 DefaultAllow: defaultDecision == acl.Allow, 755 } 756 if ixnMatch == nil { 757 // No intention found, fall back to default 758 resp.Allowed = resp.DefaultAllow 759 return resp, nil 760 } 761 762 // Intention found, combine action + permissions 763 resp.Allowed = ixnMatch.Action == structs.IntentionActionAllow 764 if len(ixnMatch.Permissions) > 0 { 765 // If any permissions are present, fall back to allowPermissions. 766 // We are not evaluating requests so we cannot know whether the L7 permission requirements will be met. 767 resp.Allowed = allowPermissions 768 resp.HasPermissions = true 769 } 770 resp.ExternalSource = ixnMatch.Meta[structs.MetaExternalSource] 771 772 // Intentions with wildcard namespaces but specific names are not allowed (*/web -> */api) 773 // So we don't check namespaces to see if there's an exact intention 774 if ixnMatch.SourceName != structs.WildcardSpecifier && ixnMatch.DestinationName != structs.WildcardSpecifier { 775 resp.HasExact = true 776 } 777 778 return resp, nil 779} 780 781// IntentionMatch returns the list of intentions that match the namespace and 782// name for either a source or destination. This applies the resolution rules 783// so wildcards will match any value. 784// 785// The returned value is the list of intentions in the same order as the 786// entries in args. The intentions themselves are sorted based on the 787// intention precedence rules. i.e. result[0][0] is the highest precedent 788// rule to match for the first entry. 789func (s *Store) IntentionMatch(ws memdb.WatchSet, args *structs.IntentionQueryMatch) (uint64, []structs.Intentions, error) { 790 tx := s.db.Txn(false) 791 defer tx.Abort() 792 793 usingConfigEntries, err := areIntentionsInConfigEntries(tx, ws) 794 if err != nil { 795 return 0, nil, err 796 } 797 if !usingConfigEntries { 798 return s.legacyIntentionMatchTxn(tx, ws, args) 799 } 800 return s.configIntentionMatchTxn(tx, ws, args) 801} 802 803func (s *Store) legacyIntentionMatchTxn(tx ReadTxn, ws memdb.WatchSet, args *structs.IntentionQueryMatch) (uint64, []structs.Intentions, error) { 804 // Get the table index. 805 idx := maxIndexTxn(tx, tableConnectIntentions) 806 if idx < 1 { 807 idx = 1 808 } 809 810 // Make all the calls and accumulate the results 811 results := make([]structs.Intentions, len(args.Entries)) 812 for i, entry := range args.Entries { 813 ixns, err := intentionMatchOneTxn(tx, ws, entry, args.Type) 814 if err != nil { 815 return 0, nil, err 816 } 817 818 // Sort the results by precedence 819 sort.Sort(structs.IntentionPrecedenceSorter(ixns)) 820 821 // Store the result 822 results[i] = ixns 823 } 824 825 return idx, results, nil 826} 827 828// IntentionMatchOne returns the list of intentions that match the namespace and 829// name for a single source or destination. This applies the resolution rules 830// so wildcards will match any value. 831// 832// The returned intentions are sorted based on the intention precedence rules. 833// i.e. result[0] is the highest precedent rule to match 834func (s *Store) IntentionMatchOne( 835 ws memdb.WatchSet, 836 entry structs.IntentionMatchEntry, 837 matchType structs.IntentionMatchType, 838) (uint64, structs.Intentions, error) { 839 tx := s.db.Txn(false) 840 defer tx.Abort() 841 842 return compatIntentionMatchOneTxn(tx, ws, entry, matchType) 843} 844 845func compatIntentionMatchOneTxn( 846 tx ReadTxn, 847 ws memdb.WatchSet, 848 entry structs.IntentionMatchEntry, 849 matchType structs.IntentionMatchType, 850) (uint64, structs.Intentions, error) { 851 852 usingConfigEntries, err := areIntentionsInConfigEntries(tx, ws) 853 if err != nil { 854 return 0, nil, err 855 } 856 if !usingConfigEntries { 857 return legacyIntentionMatchOneTxn(tx, ws, entry, matchType) 858 } 859 return configIntentionMatchOneTxn(tx, ws, entry, matchType) 860} 861 862func legacyIntentionMatchOneTxn( 863 tx ReadTxn, 864 ws memdb.WatchSet, 865 entry structs.IntentionMatchEntry, 866 matchType structs.IntentionMatchType, 867) (uint64, structs.Intentions, error) { 868 // Get the table index. 869 idx := maxIndexTxn(tx, tableConnectIntentions) 870 if idx < 1 { 871 idx = 1 872 } 873 874 results, err := intentionMatchOneTxn(tx, ws, entry, matchType) 875 if err != nil { 876 return 0, nil, err 877 } 878 879 sort.Sort(structs.IntentionPrecedenceSorter(results)) 880 881 return idx, results, nil 882} 883 884func intentionMatchOneTxn(tx ReadTxn, ws memdb.WatchSet, 885 entry structs.IntentionMatchEntry, matchType structs.IntentionMatchType) (structs.Intentions, error) { 886 887 // Each search entry may require multiple queries to memdb, so this 888 // returns the arguments for each necessary Get. Note on performance: 889 // this is not the most optimal set of queries since we repeat some 890 // many times (such as */*). We can work on improving that in the 891 // future, the test cases shouldn't have to change for that. 892 getParams, err := intentionMatchGetParams(entry) 893 if err != nil { 894 return nil, err 895 } 896 897 // Perform each call and accumulate the result. 898 var result structs.Intentions 899 for _, params := range getParams { 900 iter, err := tx.Get(tableConnectIntentions, string(matchType), params...) 901 if err != nil { 902 return nil, fmt.Errorf("failed intention lookup: %s", err) 903 } 904 905 ws.Add(iter.WatchCh()) 906 907 for ixn := iter.Next(); ixn != nil; ixn = iter.Next() { 908 result = append(result, ixn.(*structs.Intention)) 909 } 910 } 911 return result, nil 912} 913 914// intentionMatchGetParams returns the tx.Get parameters to find all the 915// intentions for a certain entry. 916func intentionMatchGetParams(entry structs.IntentionMatchEntry) ([][]interface{}, error) { 917 // We always query for "*/*" so include that. If the namespace is a 918 // wildcard, then we're actually done. 919 result := make([][]interface{}, 0, 3) 920 result = append(result, []interface{}{structs.WildcardSpecifier, structs.WildcardSpecifier}) 921 if entry.Namespace == structs.WildcardSpecifier { 922 return result, nil 923 } 924 925 // Search for NS/* intentions. If we have a wildcard name, then we're done. 926 result = append(result, []interface{}{entry.Namespace, structs.WildcardSpecifier}) 927 if entry.Name == structs.WildcardSpecifier { 928 return result, nil 929 } 930 931 // Search for the exact NS/N value. 932 result = append(result, []interface{}{entry.Namespace, entry.Name}) 933 return result, nil 934} 935 936type ServiceWithDecision struct { 937 Name structs.ServiceName 938 Decision structs.IntentionDecisionSummary 939} 940 941// IntentionTopology returns the upstreams or downstreams of a service. Upstreams and downstreams are inferred from 942// intentions. If intentions allow a connection from the target to some candidate service, the candidate service is considered 943// an upstream of the target. 944func (s *Store) IntentionTopology(ws memdb.WatchSet, 945 target structs.ServiceName, downstreams bool, defaultDecision acl.EnforcementDecision) (uint64, structs.ServiceList, error) { 946 tx := s.db.ReadTxn() 947 defer tx.Abort() 948 949 idx, services, err := s.intentionTopologyTxn(tx, ws, target, downstreams, defaultDecision) 950 if err != nil { 951 requested := "upstreams" 952 if downstreams { 953 requested = "downstreams" 954 } 955 return 0, nil, fmt.Errorf("failed to fetch %s for %s: %v", requested, target.String(), err) 956 } 957 958 resp := make(structs.ServiceList, 0) 959 for _, svc := range services { 960 resp = append(resp, svc.Name) 961 } 962 return idx, resp, nil 963} 964 965func (s *Store) intentionTopologyTxn(tx ReadTxn, ws memdb.WatchSet, 966 target structs.ServiceName, downstreams bool, defaultDecision acl.EnforcementDecision) (uint64, []ServiceWithDecision, error) { 967 968 var maxIdx uint64 969 970 // If querying the upstreams for a service, we first query intentions that apply to the target service as a source. 971 // That way we can check whether intentions from the source allow connections to upstream candidates. 972 // The reverse is true for downstreams. 973 intentionMatchType := structs.IntentionMatchSource 974 if downstreams { 975 intentionMatchType = structs.IntentionMatchDestination 976 } 977 entry := structs.IntentionMatchEntry{ 978 Namespace: target.NamespaceOrDefault(), 979 Name: target.Name, 980 } 981 index, intentions, err := compatIntentionMatchOneTxn(tx, ws, entry, intentionMatchType) 982 if err != nil { 983 return 0, nil, fmt.Errorf("failed to query intentions for %s", target.String()) 984 } 985 if index > maxIdx { 986 maxIdx = index 987 } 988 989 // Check for a wildcard intention (* -> *) since it overrides the default decision from ACLs 990 if len(intentions) > 0 { 991 // Intentions with wildcard source and destination have the lowest precedence, so they are last in the list 992 ixn := intentions[len(intentions)-1] 993 994 if ixn.HasWildcardSource() && ixn.HasWildcardDestination() { 995 defaultDecision = acl.Allow 996 if ixn.Action == structs.IntentionActionDeny { 997 defaultDecision = acl.Deny 998 } 999 } 1000 } 1001 1002 index, allServices, err := serviceListTxn(tx, ws, func(svc *structs.ServiceNode) bool { 1003 // Only include ingress gateways as downstreams, since they cannot receive service mesh traffic 1004 // TODO(freddy): One remaining issue is that this includes non-Connect services (typical services without a proxy) 1005 // Ideally those should be excluded as well, since they can't be upstreams/downstreams without a proxy. 1006 // Maybe start tracking services represented by proxies? (both sidecar and ingress) 1007 if svc.ServiceKind == structs.ServiceKindTypical || (svc.ServiceKind == structs.ServiceKindIngressGateway && downstreams) { 1008 return true 1009 } 1010 return false 1011 }, structs.WildcardEnterpriseMeta()) 1012 if err != nil { 1013 return index, nil, fmt.Errorf("failed to fetch catalog service list: %v", err) 1014 } 1015 if index > maxIdx { 1016 maxIdx = index 1017 } 1018 1019 // When checking authorization to upstreams, the match type for the decision is `destination` because we are deciding 1020 // if upstream candidates are covered by intentions that have the target service as a source. 1021 // The reverse is true for downstreams. 1022 decisionMatchType := structs.IntentionMatchDestination 1023 if downstreams { 1024 decisionMatchType = structs.IntentionMatchSource 1025 } 1026 result := make([]ServiceWithDecision, 0, len(allServices)) 1027 for _, candidate := range allServices { 1028 if candidate.Name == structs.ConsulServiceName { 1029 continue 1030 } 1031 decision, err := s.IntentionDecision(candidate.Name, candidate.NamespaceOrDefault(), intentions, decisionMatchType, defaultDecision, true) 1032 if err != nil { 1033 src, dst := target, candidate 1034 if downstreams { 1035 src, dst = candidate, target 1036 } 1037 return 0, nil, fmt.Errorf("failed to get intention decision from (%s) to (%s): %v", 1038 src.String(), dst.String(), err) 1039 } 1040 if !decision.Allowed || target.Matches(candidate) { 1041 continue 1042 } 1043 1044 result = append(result, ServiceWithDecision{ 1045 Name: candidate, 1046 Decision: decision, 1047 }) 1048 } 1049 return maxIdx, result, err 1050} 1051