1package vault 2 3import ( 4 "context" 5 "fmt" 6 "strings" 7 8 "github.com/golang/protobuf/ptypes" 9 "github.com/hashicorp/vault/helper/identity" 10 "github.com/hashicorp/vault/helper/namespace" 11 "github.com/hashicorp/vault/helper/storagepacker" 12 "github.com/hashicorp/vault/sdk/framework" 13 "github.com/hashicorp/vault/sdk/logical" 14) 15 16// aliasPaths returns the API endpoints to operate on aliases. 17// Following are the paths supported: 18// entity-alias - To register/modify an alias 19// entity-alias/id - To read, modify, delete and list aliases based on their ID 20func aliasPaths(i *IdentityStore) []*framework.Path { 21 return []*framework.Path{ 22 { 23 Pattern: "entity-alias$", 24 Fields: map[string]*framework.FieldSchema{ 25 "id": { 26 Type: framework.TypeString, 27 Description: "ID of the entity alias. If set, updates the corresponding entity alias.", 28 }, 29 // entity_id is deprecated in favor of canonical_id 30 "entity_id": { 31 Type: framework.TypeString, 32 Description: `Entity ID to which this alias belongs. 33This field is deprecated, use canonical_id.`, 34 }, 35 "canonical_id": { 36 Type: framework.TypeString, 37 Description: "Entity ID to which this alias belongs", 38 }, 39 "mount_accessor": { 40 Type: framework.TypeString, 41 Description: "Mount accessor to which this alias belongs to; unused for a modify", 42 }, 43 "name": { 44 Type: framework.TypeString, 45 Description: "Name of the alias; unused for a modify", 46 }, 47 }, 48 Callbacks: map[logical.Operation]framework.OperationFunc{ 49 logical.UpdateOperation: i.handleAliasCreateUpdate(), 50 }, 51 52 HelpSynopsis: strings.TrimSpace(aliasHelp["alias"][0]), 53 HelpDescription: strings.TrimSpace(aliasHelp["alias"][1]), 54 }, 55 { 56 Pattern: "entity-alias/id/" + framework.GenericNameRegex("id"), 57 Fields: map[string]*framework.FieldSchema{ 58 "id": { 59 Type: framework.TypeString, 60 Description: "ID of the alias", 61 }, 62 // entity_id is deprecated 63 "entity_id": { 64 Type: framework.TypeString, 65 Description: `Entity ID to which this alias belongs to. 66This field is deprecated, use canonical_id.`, 67 }, 68 "canonical_id": { 69 Type: framework.TypeString, 70 Description: "Entity ID to which this alias should be tied to", 71 }, 72 "mount_accessor": { 73 Type: framework.TypeString, 74 Description: "(Unused)", 75 }, 76 "name": { 77 Type: framework.TypeString, 78 Description: "(Unused)", 79 }, 80 }, 81 Callbacks: map[logical.Operation]framework.OperationFunc{ 82 logical.UpdateOperation: i.handleAliasCreateUpdate(), 83 logical.ReadOperation: i.pathAliasIDRead(), 84 logical.DeleteOperation: i.pathAliasIDDelete(), 85 }, 86 87 HelpSynopsis: strings.TrimSpace(aliasHelp["alias-id"][0]), 88 HelpDescription: strings.TrimSpace(aliasHelp["alias-id"][1]), 89 }, 90 { 91 Pattern: "entity-alias/id/?$", 92 Callbacks: map[logical.Operation]framework.OperationFunc{ 93 logical.ListOperation: i.pathAliasIDList(), 94 }, 95 96 HelpSynopsis: strings.TrimSpace(aliasHelp["alias-id-list"][0]), 97 HelpDescription: strings.TrimSpace(aliasHelp["alias-id-list"][1]), 98 }, 99 } 100} 101 102// handleAliasCreateUpdate is used to create or update an alias 103func (i *IdentityStore) handleAliasCreateUpdate() framework.OperationFunc { 104 return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { 105 var err error 106 107 ns, err := namespace.FromContext(ctx) 108 if err != nil { 109 return nil, err 110 } 111 112 // Get alias name, if any 113 name := d.Get("name").(string) 114 115 // Get mount accessor, if any 116 mountAccessor := d.Get("mount_accessor").(string) 117 118 // Get ID, if any 119 id := d.Get("id").(string) 120 121 // Get entity id 122 canonicalID := d.Get("canonical_id").(string) 123 if canonicalID == "" { 124 // For backwards compatibility 125 canonicalID = d.Get("entity_id").(string) 126 } 127 128 i.lock.Lock() 129 defer i.lock.Unlock() 130 131 // This block is run if they provided an ID 132 { 133 // If they provide an ID it must be an update. Find the alias, perform 134 // due diligence, call the update function. 135 if id != "" { 136 alias, err := i.MemDBAliasByID(id, true, false) 137 if err != nil { 138 return nil, err 139 } 140 if alias == nil { 141 return logical.ErrorResponse("invalid alias ID provided"), nil 142 } 143 if alias.NamespaceID != ns.ID { 144 return logical.ErrorResponse("cannot modify aliases across namespaces"), logical.ErrPermissionDenied 145 } 146 147 switch { 148 case mountAccessor == "" && name == "": 149 // Just a canonical ID update, maybe 150 if canonicalID == "" { 151 // Nothing to do, so be idempotent 152 return nil, nil 153 } 154 155 name = alias.Name 156 mountAccessor = alias.MountAccessor 157 158 case mountAccessor == "": 159 // No change to mount accessor 160 mountAccessor = alias.MountAccessor 161 162 case name == "": 163 // No change to mount name 164 name = alias.Name 165 166 default: 167 // Both provided 168 } 169 170 return i.handleAliasUpdate(ctx, req, canonicalID, name, mountAccessor, alias) 171 } 172 } 173 174 // If they didn't provide an ID, we must have both accessor and name provided 175 if mountAccessor == "" || name == "" { 176 return logical.ErrorResponse("'id' or 'mount_accessor' and 'name' must be provided"), nil 177 } 178 179 // Look up the alias by factors; if it's found it's an update 180 mountEntry := i.core.router.MatchingMountByAccessor(mountAccessor) 181 if mountEntry == nil { 182 return logical.ErrorResponse(fmt.Sprintf("invalid mount accessor %q", mountAccessor)), nil 183 } 184 if mountEntry.Local { 185 return logical.ErrorResponse(fmt.Sprintf("mount accessor %q is of a local mount", mountAccessor)), nil 186 } 187 if mountEntry.NamespaceID != ns.ID { 188 return logical.ErrorResponse("matching mount is in a different namespace than request"), logical.ErrPermissionDenied 189 } 190 alias, err := i.MemDBAliasByFactors(mountAccessor, name, false, false) 191 if err != nil { 192 return nil, err 193 } 194 if alias != nil { 195 if alias.NamespaceID != ns.ID { 196 return logical.ErrorResponse("cannot modify aliases across namespaces"), logical.ErrPermissionDenied 197 } 198 199 return i.handleAliasUpdate(ctx, req, alias.CanonicalID, name, mountAccessor, alias) 200 } 201 202 // At this point we know it's a new creation request 203 return i.handleAliasCreate(ctx, req, canonicalID, name, mountAccessor) 204 } 205} 206 207func (i *IdentityStore) handleAliasCreate(ctx context.Context, req *logical.Request, canonicalID, name, mountAccessor string) (*logical.Response, error) { 208 ns, err := namespace.FromContext(ctx) 209 if err != nil { 210 return nil, err 211 } 212 213 alias := &identity.Alias{ 214 MountAccessor: mountAccessor, 215 Name: name, 216 } 217 entity := &identity.Entity{} 218 219 // If a canonical ID is provided pull up the entity and make sure we're in 220 // the right NS 221 if canonicalID != "" { 222 entity, err = i.MemDBEntityByID(canonicalID, true) 223 if err != nil { 224 return nil, err 225 } 226 if entity == nil { 227 return logical.ErrorResponse("invalid canonical ID"), nil 228 } 229 if entity.NamespaceID != ns.ID { 230 return logical.ErrorResponse("entity found with 'canonical_id' not in request namespace"), logical.ErrPermissionDenied 231 } 232 } 233 234 entity.Aliases = append(entity.Aliases, alias) 235 236 // ID creation and other validations; This is more useful for new entities 237 // and may not perform anything for the existing entities. Placing the 238 // check here to make the flow common for both new and existing entities. 239 err = i.sanitizeEntity(ctx, entity) 240 if err != nil { 241 return nil, err 242 } 243 244 // Set the canonical ID in the alias index. This should be done after 245 // sanitizing entity in case it's a new entity that didn't have an ID. 246 alias.CanonicalID = entity.ID 247 248 // ID creation and other validations 249 err = i.sanitizeAlias(ctx, alias) 250 if err != nil { 251 return nil, err 252 } 253 254 // Index entity and its aliases in MemDB and persist entity along with 255 // aliases in storage. 256 if err := i.upsertEntity(ctx, entity, nil, true); err != nil { 257 return nil, err 258 } 259 260 // Return ID of both alias and entity 261 return &logical.Response{ 262 Data: map[string]interface{}{ 263 "id": alias.ID, 264 "canonical_id": entity.ID, 265 }, 266 }, nil 267} 268 269func (i *IdentityStore) handleAliasUpdate(ctx context.Context, req *logical.Request, canonicalID, name, mountAccessor string, alias *identity.Alias) (*logical.Response, error) { 270 if name == alias.Name && 271 mountAccessor == alias.MountAccessor && 272 (canonicalID == alias.CanonicalID || canonicalID == "") { 273 // Nothing to do; return nil to be idempotent 274 return nil, nil 275 } 276 277 alias.LastUpdateTime = ptypes.TimestampNow() 278 279 // If we're changing one or the other or both of these, make sure that 280 // there isn't a matching alias already, and make sure it's in the same 281 // namespace. 282 if name != alias.Name || mountAccessor != alias.MountAccessor { 283 // Check here to see if such an alias already exists, if so bail 284 mountEntry := i.core.router.MatchingMountByAccessor(mountAccessor) 285 if mountEntry == nil { 286 return logical.ErrorResponse(fmt.Sprintf("invalid mount accessor %q", mountAccessor)), nil 287 } 288 if mountEntry.Local { 289 return logical.ErrorResponse(fmt.Sprintf("mount_accessor %q is of a local mount", mountAccessor)), nil 290 } 291 if mountEntry.NamespaceID != alias.NamespaceID { 292 return logical.ErrorResponse("given mount accessor is not in the same namespace as the existing alias"), logical.ErrPermissionDenied 293 } 294 295 existingAlias, err := i.MemDBAliasByFactors(mountAccessor, name, false, false) 296 if err != nil { 297 return nil, err 298 } 299 // Bail unless it's just a case change 300 if existingAlias != nil && !strings.EqualFold(existingAlias.Name, name) { 301 return logical.ErrorResponse("alias with combination of mount accessor and name already exists"), nil 302 } 303 304 // Update the values in the alias 305 alias.Name = name 306 alias.MountAccessor = mountAccessor 307 } 308 309 // Get our current entity, which may be the same as the new one if the 310 // canonical ID hasn't changed 311 currentEntity, err := i.MemDBEntityByAliasID(alias.ID, true) 312 if err != nil { 313 return nil, err 314 } 315 if currentEntity == nil { 316 return logical.ErrorResponse("given alias is not associated with an entity"), nil 317 } 318 if currentEntity.NamespaceID != alias.NamespaceID { 319 return logical.ErrorResponse("alias associated with an entity in a different namespace"), logical.ErrPermissionDenied 320 } 321 322 newEntity := currentEntity 323 if canonicalID != "" && canonicalID != alias.CanonicalID { 324 newEntity, err = i.MemDBEntityByID(canonicalID, true) 325 if err != nil { 326 return nil, err 327 } 328 if newEntity == nil { 329 return logical.ErrorResponse("given 'canonical_id' is not associated with an entity"), nil 330 } 331 if newEntity.NamespaceID != alias.NamespaceID { 332 return logical.ErrorResponse("given 'canonical_id' associated with entity in a different namespace from the alias"), logical.ErrPermissionDenied 333 } 334 335 // Update the canonical ID value and move it from the current enitity to the new one 336 alias.CanonicalID = newEntity.ID 337 newEntity.Aliases = append(newEntity.Aliases, alias) 338 for aliasIndex, item := range currentEntity.Aliases { 339 if item.ID == alias.ID { 340 currentEntity.Aliases = append(currentEntity.Aliases[:aliasIndex], currentEntity.Aliases[aliasIndex+1:]...) 341 break 342 } 343 } 344 } else { 345 // If it's not moving we still need to update it in the existing 346 // entity's aliases 347 for aliasIndex, item := range currentEntity.Aliases { 348 if item.ID == alias.ID { 349 currentEntity.Aliases[aliasIndex] = alias 350 break 351 } 352 } 353 // newEntity will be pointing to the same entity; set currentEntity nil 354 // so the upsertCall gets nil for the previous entity as we're only 355 // changing one. 356 currentEntity = nil 357 } 358 359 // Index entity and its aliases in MemDB and persist entity along with 360 // aliases in storage. If the alias is being transferred over from 361 // one entity to another, previous entity needs to get refreshed in MemDB 362 // and persisted in storage as well. 363 if err := i.upsertEntity(ctx, newEntity, currentEntity, true); err != nil { 364 return nil, err 365 } 366 367 // Return ID of both alias and entity 368 return &logical.Response{ 369 Data: map[string]interface{}{ 370 "id": alias.ID, 371 "canonical_id": newEntity.ID, 372 }, 373 }, nil 374} 375 376// pathAliasIDRead returns the properties of an alias for a given 377// alias ID 378func (i *IdentityStore) pathAliasIDRead() framework.OperationFunc { 379 return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { 380 aliasID := d.Get("id").(string) 381 if aliasID == "" { 382 return logical.ErrorResponse("missing alias id"), nil 383 } 384 385 alias, err := i.MemDBAliasByID(aliasID, false, false) 386 if err != nil { 387 return nil, err 388 } 389 390 return i.handleAliasReadCommon(ctx, alias) 391 } 392} 393 394func (i *IdentityStore) handleAliasReadCommon(ctx context.Context, alias *identity.Alias) (*logical.Response, error) { 395 if alias == nil { 396 return nil, nil 397 } 398 399 ns, err := namespace.FromContext(ctx) 400 if err != nil { 401 return nil, err 402 } 403 if ns.ID != alias.NamespaceID { 404 return logical.ErrorResponse("alias and request are in different namespaces"), logical.ErrPermissionDenied 405 } 406 407 respData := map[string]interface{}{} 408 respData["id"] = alias.ID 409 respData["canonical_id"] = alias.CanonicalID 410 respData["mount_accessor"] = alias.MountAccessor 411 respData["metadata"] = alias.Metadata 412 respData["name"] = alias.Name 413 respData["merged_from_canonical_ids"] = alias.MergedFromCanonicalIDs 414 respData["namespace_id"] = alias.NamespaceID 415 416 if mountValidationResp := i.core.router.validateMountByAccessor(alias.MountAccessor); mountValidationResp != nil { 417 respData["mount_path"] = mountValidationResp.MountPath 418 respData["mount_type"] = mountValidationResp.MountType 419 } 420 421 // Convert protobuf timestamp into RFC3339 format 422 respData["creation_time"] = ptypes.TimestampString(alias.CreationTime) 423 respData["last_update_time"] = ptypes.TimestampString(alias.LastUpdateTime) 424 425 return &logical.Response{ 426 Data: respData, 427 }, nil 428} 429 430// pathAliasIDDelete deletes the alias for a given alias ID 431func (i *IdentityStore) pathAliasIDDelete() framework.OperationFunc { 432 return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { 433 aliasID := d.Get("id").(string) 434 if aliasID == "" { 435 return logical.ErrorResponse("missing alias ID"), nil 436 } 437 438 i.lock.Lock() 439 defer i.lock.Unlock() 440 441 // Create a MemDB transaction to delete entity 442 txn := i.db.Txn(true) 443 defer txn.Abort() 444 445 // Fetch the alias 446 alias, err := i.MemDBAliasByIDInTxn(txn, aliasID, false, false) 447 if err != nil { 448 return nil, err 449 } 450 451 // If there is no alias for the ID, do nothing 452 if alias == nil { 453 return nil, nil 454 } 455 456 ns, err := namespace.FromContext(ctx) 457 if err != nil { 458 return nil, err 459 } 460 if ns.ID != alias.NamespaceID { 461 return logical.ErrorResponse("request and alias are in different namespaces"), logical.ErrPermissionDenied 462 } 463 464 // Fetch the associated entity 465 entity, err := i.MemDBEntityByAliasIDInTxn(txn, alias.ID, true) 466 if err != nil { 467 return nil, err 468 } 469 470 // If there is no entity tied to a valid alias, something is wrong 471 if entity == nil { 472 return nil, fmt.Errorf("alias not associated to an entity") 473 } 474 475 aliases := []*identity.Alias{ 476 alias, 477 } 478 479 // Delete alias from the entity object 480 err = i.deleteAliasesInEntityInTxn(txn, entity, aliases) 481 if err != nil { 482 return nil, err 483 } 484 485 // Update the entity index in the entities table 486 err = i.MemDBUpsertEntityInTxn(txn, entity) 487 if err != nil { 488 return nil, err 489 } 490 491 // Persist the entity object 492 entityAsAny, err := ptypes.MarshalAny(entity) 493 if err != nil { 494 return nil, err 495 } 496 item := &storagepacker.Item{ 497 ID: entity.ID, 498 Message: entityAsAny, 499 } 500 501 err = i.entityPacker.PutItem(ctx, item) 502 if err != nil { 503 return nil, err 504 } 505 506 // Committing the transaction *after* successfully updating entity in 507 // storage 508 txn.Commit() 509 510 return nil, nil 511 } 512} 513 514// pathAliasIDList lists the IDs of all the valid aliases in the identity 515// store 516func (i *IdentityStore) pathAliasIDList() framework.OperationFunc { 517 return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) { 518 return i.handleAliasListCommon(ctx, false) 519 } 520} 521 522var aliasHelp = map[string][2]string{ 523 "alias": { 524 "Create a new alias.", 525 "", 526 }, 527 "alias-id": { 528 "Update, read or delete an alias ID.", 529 "", 530 }, 531 "alias-id-list": { 532 "List all the alias IDs.", 533 "", 534 }, 535} 536