// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package vault

import (
	"context"
	"fmt"
	"strings"

	"github.com/golang/protobuf/ptypes"
	"github.com/hashicorp/go-secure-stdlib/strutil"
	"github.com/hashicorp/vault/helper/identity"
	"github.com/hashicorp/vault/helper/namespace"
	"github.com/hashicorp/vault/helper/storagepacker"
	"github.com/hashicorp/vault/sdk/framework"
	"github.com/hashicorp/vault/sdk/helper/custommetadata"
	"github.com/hashicorp/vault/sdk/logical"
)

// aliasPaths returns the API endpoints to operate on aliases.
// Following are the paths supported:
// entity-alias - To register/modify an alias
// entity-alias/id - To read, modify, delete and list aliases based on their ID
func aliasPaths(i *IdentityStore) []*framework.Path {
	return []*framework.Path{
		{
			Pattern: "entity-alias$",

			DisplayAttrs: &framework.DisplayAttributes{
				OperationPrefix: "entity",
				OperationVerb:   "create",
				OperationSuffix: "alias",
			},

			Fields: map[string]*framework.FieldSchema{
				"id": {
					Type:        framework.TypeString,
					Description: "ID of the entity alias. If set, updates the corresponding entity alias.",
				},
				// entity_id is deprecated in favor of canonical_id
				"entity_id": {
					Type: framework.TypeString,
					Description: `Entity ID to which this alias belongs.
This field is deprecated, use canonical_id.`,
				},
				"canonical_id": {
					Type:        framework.TypeString,
					Description: "Entity ID to which this alias belongs",
				},
				"mount_accessor": {
					Type:        framework.TypeString,
					Description: "Mount accessor to which this alias belongs to; unused for a modify",
				},
				"name": {
					Type:        framework.TypeString,
					Description: "Name of the alias; unused for a modify",
				},
				"custom_metadata": {
					Type:        framework.TypeKVPairs,
					Description: "User provided key-value pairs",
				},
			},
			Callbacks: map[logical.Operation]framework.OperationFunc{
				logical.UpdateOperation: i.handleAliasCreateUpdate(),
			},

			HelpSynopsis:    strings.TrimSpace(aliasHelp["alias"][0]),
			HelpDescription: strings.TrimSpace(aliasHelp["alias"][1]),
		},
		{
			Pattern: "entity-alias/id/" + framework.GenericNameRegex("id"),

			DisplayAttrs: &framework.DisplayAttributes{
				OperationPrefix: "entity",
				OperationSuffix: "alias-by-id",
			},

			Fields: map[string]*framework.FieldSchema{
				"id": {
					Type:        framework.TypeString,
					Description: "ID of the alias",
				},
				// entity_id is deprecated
				"entity_id": {
					Type: framework.TypeString,
					Description: `Entity ID to which this alias belongs to.
This field is deprecated, use canonical_id.`,
				},
				"canonical_id": {
					Type:        framework.TypeString,
					Description: "Entity ID to which this alias should be tied to",
				},
				"mount_accessor": {
					Type:        framework.TypeString,
					Description: "(Unused)",
				},
				"name": {
					Type:        framework.TypeString,
					Description: "(Unused)",
				},
				"custom_metadata": {
					Type:        framework.TypeKVPairs,
					Description: "User provided key-value pairs",
				},
			},

			Operations: map[logical.Operation]framework.OperationHandler{
				logical.UpdateOperation: &framework.PathOperation{
					Callback: i.handleAliasCreateUpdate(),
					DisplayAttrs: &framework.DisplayAttributes{
						OperationVerb: "update",
					},
				},
				logical.ReadOperation: &framework.PathOperation{
					Callback: i.pathAliasIDRead(),
					DisplayAttrs: &framework.DisplayAttributes{
						OperationVerb: "read",
					},
				},
				logical.DeleteOperation: &framework.PathOperation{
					Callback: i.pathAliasIDDelete(),
					DisplayAttrs: &framework.DisplayAttributes{
						OperationVerb: "delete",
					},
				},
			},

			HelpSynopsis:    strings.TrimSpace(aliasHelp["alias-id"][0]),
			HelpDescription: strings.TrimSpace(aliasHelp["alias-id"][1]),
		},
		{
			Pattern: "entity-alias/id/?$",

			DisplayAttrs: &framework.DisplayAttributes{
				OperationPrefix: "entity",
				OperationVerb:   "list",
				OperationSuffix: "aliases-by-id",
			},

			Callbacks: map[logical.Operation]framework.OperationFunc{
				logical.ListOperation: i.pathAliasIDList(),
			},

			HelpSynopsis:    strings.TrimSpace(aliasHelp["alias-id-list"][0]),
			HelpDescription: strings.TrimSpace(aliasHelp["alias-id-list"][1]),
		},
	}
}

// handleAliasCreateUpdate is used to create or update an alias
func (i *IdentityStore) handleAliasCreateUpdate() framework.OperationFunc {
	return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
		var err error

		ns, err := namespace.FromContext(ctx)
		if err != nil {
			return nil, err
		}

		// Get alias name, if any
		name := d.Get("name").(string)

		// Get mount accessor, if any
		mountAccessor := d.Get("mount_accessor").(string)

		// Get ID, if any
		id := d.Get("id").(string)

		// Get custom metadata, if any
		customMetadata := make(map[string]string)
		data, customMetadataExists := d.GetOk("custom_metadata")
		if customMetadataExists {
			customMetadata = data.(map[string]string)
		}

		// Get entity id
		canonicalID := d.Get("canonical_id").(string)
		if canonicalID == "" {
			// For backwards compatibility
			canonicalID = d.Get("entity_id").(string)
		}

		// validate customMetadata if provided
		if len(customMetadata) != 0 {
			if err := custommetadata.Validate(customMetadata); err != nil {
				return nil, err
			}
		}

		i.lock.Lock()
		defer i.lock.Unlock()

		// This block is run if they provided an ID
		{
			// If they provide an ID it must be an update. Find the alias, perform
			// due diligence, call the update function.
			if id != "" {
				alias, err := i.MemDBAliasByID(id, true, false)
				if err != nil {
					return nil, err
				}
				if alias == nil {
					return logical.ErrorResponse("invalid alias ID provided"), nil
				}
				if alias.NamespaceID != ns.ID {
					return logical.ErrorResponse("cannot modify aliases across namespaces"), logical.ErrPermissionDenied
				}
				if !customMetadataExists {
					customMetadata = alias.CustomMetadata
				}
				switch {
				case mountAccessor == "" && name == "":
					// Check if the canonicalID or the customMetadata are being
					// updated
					if canonicalID == "" && !customMetadataExists {
						// Nothing to do, so be idempotent
						return nil, nil
					}
					name = alias.Name
					mountAccessor = alias.MountAccessor
				case mountAccessor == "":
					// No change to mount accessor
					mountAccessor = alias.MountAccessor
				case name == "":
					// No change to mount name
					name = alias.Name
				default:
					// mountAccessor, name and customMetadata  provided
				}
				return i.handleAliasUpdate(ctx, canonicalID, name, mountAccessor, alias, customMetadata)
			}
		}

		// If they didn't provide an ID, we must have both accessor and name provided
		if mountAccessor == "" || name == "" {
			return logical.ErrorResponse("'id' or 'mount_accessor' and 'name' must be provided"), nil
		}

		// Look up the alias by factors; if it's found it's an update
		mountEntry := i.router.MatchingMountByAccessor(mountAccessor)
		if mountEntry == nil {
			return logical.ErrorResponse(fmt.Sprintf("invalid mount accessor %q", mountAccessor)), nil
		}
		if mountEntry.NamespaceID != ns.ID {
			return logical.ErrorResponse("matching mount is in a different namespace than request"), logical.ErrPermissionDenied
		}
		alias, err := i.MemDBAliasByFactors(mountAccessor, name, false, false)
		if err != nil {
			return nil, err
		}
		if alias != nil {
			if alias.NamespaceID != ns.ID {
				return logical.ErrorResponse("cannot modify aliases across namespaces"), logical.ErrPermissionDenied
			}
			return i.handleAliasUpdate(ctx, canonicalID, name, mountAccessor, alias, customMetadata)
		}
		// At this point we know it's a new creation request
		return i.handleAliasCreate(ctx, canonicalID, name, mountAccessor, mountEntry.Local, customMetadata)
	}
}

func (i *IdentityStore) handleAliasCreate(ctx context.Context, canonicalID, name, mountAccessor string, local bool, customMetadata map[string]string) (*logical.Response, error) {
	ns, err := namespace.FromContext(ctx)
	if err != nil {
		return nil, err
	}

	var entity *identity.Entity
	if canonicalID != "" {
		entity, err = i.MemDBEntityByID(canonicalID, true)
		if err != nil {
			return nil, err
		}
		if entity == nil {
			return logical.ErrorResponse("invalid canonical ID"), nil
		}
		if entity.NamespaceID != ns.ID {
			return logical.ErrorResponse("entity found with 'canonical_id' not in request namespace"), logical.ErrPermissionDenied
		}
	}

	if entity == nil && local {
		// Check to see if the entity creation should be forwarded.
		entity, err = i.entityCreator.CreateEntity(ctx)
		if err != nil {
			return nil, err
		}
	}

	persist := false
	// If the request was not forwarded, then this is the active node of the
	// primary. Create the entity here itself.
	if entity == nil {
		persist = true
		entity = new(identity.Entity)
		err = i.sanitizeEntity(ctx, entity)
		if err != nil {
			return nil, err
		}
	}

	for _, currentAlias := range entity.Aliases {
		if currentAlias.MountAccessor == mountAccessor {
			return logical.ErrorResponse("Alias already exists for requested entity and mount accessor"), nil
		}
	}

	var alias *identity.Alias
	switch local {
	case true:
		alias, err = i.processLocalAlias(ctx, &logical.Alias{
			MountAccessor:  mountAccessor,
			Name:           name,
			Local:          local,
			CustomMetadata: customMetadata,
		}, entity, false)
		if err != nil {
			return nil, err
		}
	default:
		alias = &identity.Alias{
			MountAccessor:  mountAccessor,
			Name:           name,
			CustomMetadata: customMetadata,
			CanonicalID:    entity.ID,
		}
		err = i.sanitizeAlias(ctx, alias)
		if err != nil {
			return nil, err
		}
		entity.UpsertAlias(alias)
		persist = true
	}

	// Index entity and its aliases in MemDB and persist entity along with
	// aliases in storage.
	if err := i.upsertEntity(ctx, entity, nil, persist); err != nil {
		return nil, err
	}

	// Return ID of both alias and entity
	return &logical.Response{
		Data: map[string]interface{}{
			"id":           alias.ID,
			"canonical_id": entity.ID,
		},
	}, nil
}

func (i *IdentityStore) handleAliasUpdate(ctx context.Context, canonicalID, name, mountAccessor string, alias *identity.Alias, customMetadata map[string]string) (*logical.Response, error) {
	if name == alias.Name &&
		mountAccessor == alias.MountAccessor &&
		(canonicalID == alias.CanonicalID || canonicalID == "") && (strutil.EqualStringMaps(customMetadata, alias.CustomMetadata)) {
		// Nothing to do; return nil to be idempotent
		return nil, nil
	}

	alias.LastUpdateTime = ptypes.TimestampNow()

	// Get our current entity, which may be the same as the new one if the
	// canonical ID hasn't changed
	currentEntity, err := i.MemDBEntityByAliasID(alias.ID, true)
	if err != nil {
		return nil, err
	}
	if currentEntity == nil {
		return logical.ErrorResponse("given alias is not associated with an entity"), nil
	}

	if currentEntity.NamespaceID != alias.NamespaceID {
		return logical.ErrorResponse("alias and entity do not belong to the same namespace"), logical.ErrPermissionDenied
	}

	// If the accessor is being changed but the entity is not, check if the entity
	// already has an alias corresponding to the new accessor
	if mountAccessor != alias.MountAccessor && (canonicalID == "" || canonicalID == alias.CanonicalID) {
		for _, currentAlias := range currentEntity.Aliases {
			if currentAlias.MountAccessor == mountAccessor {
				return logical.ErrorResponse("Alias cannot be updated as the entity already has an alias for the given 'mount_accessor' "), nil
			}
		}
	}
	// If we're changing one or the other or both of these, make sure that
	// there isn't a matching alias already, and make sure it's in the same
	// namespace.
	if name != alias.Name || mountAccessor != alias.MountAccessor || !strutil.EqualStringMaps(customMetadata, alias.CustomMetadata) {
		// Check here to see if such an alias already exists, if so bail
		mountEntry := i.router.MatchingMountByAccessor(mountAccessor)
		if mountEntry == nil {
			return logical.ErrorResponse(fmt.Sprintf("invalid mount accessor %q", mountAccessor)), nil
		}
		if mountEntry.NamespaceID != alias.NamespaceID {
			return logical.ErrorResponse("given mount accessor is not in the same namespace as the existing alias"), logical.ErrPermissionDenied
		}

		existingAlias, err := i.MemDBAliasByFactors(mountAccessor, name, false, false)
		if err != nil {
			return nil, err
		}

		// Bail unless it's just a case change
		if existingAlias != nil && existingAlias.ID != alias.ID {
			return logical.ErrorResponse("alias with combination of mount accessor and name already exists"), nil
		}

		// Update the values in the alias
		alias.Name = name
		alias.MountAccessor = mountAccessor
		alias.CustomMetadata = customMetadata
	}

	mountValidationResp := i.router.ValidateMountByAccessor(alias.MountAccessor)
	if mountValidationResp == nil {
		return nil, fmt.Errorf("invalid mount accessor %q", alias.MountAccessor)
	}

	newEntity := currentEntity
	if canonicalID != "" && canonicalID != alias.CanonicalID {
		// Don't allow moving local aliases between entities.
		if mountValidationResp.MountLocal {
			return logical.ErrorResponse("local aliases can't be moved between entities"), nil
		}

		newEntity, err = i.MemDBEntityByID(canonicalID, true)
		if err != nil {
			return nil, err
		}
		if newEntity == nil {
			return logical.ErrorResponse("given 'canonical_id' is not associated with an entity"), nil
		}
		if newEntity.NamespaceID != alias.NamespaceID {
			return logical.ErrorResponse("given 'canonical_id' associated with entity in a different namespace from the alias"), logical.ErrPermissionDenied
		}

		// Check if the entity the alias is being updated to, already has an alias for the mount
		for _, alias := range newEntity.Aliases {
			if alias.MountAccessor == mountAccessor {
				return logical.ErrorResponse("Alias cannot be updated as the given entity already has an alias for this mount "), nil
			}
		}

		// Update the canonical ID value and move it from the current entity to the new one
		alias.CanonicalID = newEntity.ID
		newEntity.Aliases = append(newEntity.Aliases, alias)
		for aliasIndex, item := range currentEntity.Aliases {
			if item.ID == alias.ID {
				currentEntity.Aliases = append(currentEntity.Aliases[:aliasIndex], currentEntity.Aliases[aliasIndex+1:]...)
				break
			}
		}
	} else {
		// If it's not moving we still need to update it in the existing
		// entity's aliases
		for aliasIndex, item := range currentEntity.Aliases {
			if item.ID == alias.ID {
				currentEntity.Aliases[aliasIndex] = alias
				break
			}
		}
		// newEntity will be pointing to the same entity; set currentEntity nil
		// so the upsertCall gets nil for the previous entity as we're only
		// changing one.
		currentEntity = nil
	}

	if mountValidationResp.MountLocal {
		alias, err = i.processLocalAlias(ctx, &logical.Alias{
			MountAccessor:  mountAccessor,
			Name:           name,
			Local:          mountValidationResp.MountLocal,
			CustomMetadata: customMetadata,
		}, newEntity, true)
		if err != nil {
			return nil, err
		}

		return &logical.Response{
			Data: map[string]interface{}{
				"id":           alias.ID,
				"canonical_id": newEntity.ID,
			},
		}, nil
	}

	// Index entity and its aliases in MemDB and persist entity along with
	// aliases in storage. If the alias is being transferred over from
	// one entity to another, previous entity needs to get refreshed in MemDB
	// and persisted in storage as well.
	if err := i.upsertEntity(ctx, newEntity, currentEntity, true); err != nil {
		return nil, err
	}

	// Return ID of both alias and entity
	return &logical.Response{
		Data: map[string]interface{}{
			"id":           alias.ID,
			"canonical_id": newEntity.ID,
		},
	}, nil
}

// pathAliasIDRead returns the properties of an alias for a given
// alias ID
func (i *IdentityStore) pathAliasIDRead() framework.OperationFunc {
	return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
		aliasID := d.Get("id").(string)
		if aliasID == "" {
			return logical.ErrorResponse("missing alias id"), nil
		}

		alias, err := i.MemDBAliasByID(aliasID, false, false)
		if err != nil {
			return nil, err
		}

		return i.handleAliasReadCommon(ctx, alias)
	}
}

func (i *IdentityStore) handleAliasReadCommon(ctx context.Context, alias *identity.Alias) (*logical.Response, error) {
	if alias == nil {
		return nil, nil
	}

	ns, err := namespace.FromContext(ctx)
	if err != nil {
		return nil, err
	}
	if ns.ID != alias.NamespaceID {
		return logical.ErrorResponse("alias and request are in different namespaces"), logical.ErrPermissionDenied
	}

	respData := map[string]interface{}{}
	respData["id"] = alias.ID
	respData["canonical_id"] = alias.CanonicalID
	respData["mount_accessor"] = alias.MountAccessor
	respData["metadata"] = alias.Metadata
	respData["custom_metadata"] = alias.CustomMetadata
	respData["name"] = alias.Name
	respData["merged_from_canonical_ids"] = alias.MergedFromCanonicalIDs
	respData["namespace_id"] = alias.NamespaceID
	respData["local"] = alias.Local

	if mountValidationResp := i.router.ValidateMountByAccessor(alias.MountAccessor); mountValidationResp != nil {
		respData["mount_path"] = mountValidationResp.MountPath
		respData["mount_type"] = mountValidationResp.MountType
	}

	// Convert protobuf timestamp into RFC3339 format
	respData["creation_time"] = ptypes.TimestampString(alias.CreationTime)
	respData["last_update_time"] = ptypes.TimestampString(alias.LastUpdateTime)

	return &logical.Response{
		Data: respData,
	}, nil
}

// pathAliasIDDelete deletes the alias for a given alias ID
func (i *IdentityStore) pathAliasIDDelete() framework.OperationFunc {
	return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
		aliasID := d.Get("id").(string)
		if aliasID == "" {
			return logical.ErrorResponse("missing alias ID"), nil
		}

		i.lock.Lock()
		defer i.lock.Unlock()

		// Create a MemDB transaction to delete entity
		txn := i.db.Txn(true)
		defer txn.Abort()

		// Fetch the alias
		alias, err := i.MemDBAliasByIDInTxn(txn, aliasID, false, false)
		if err != nil {
			return nil, err
		}

		// If there is no alias for the ID, do nothing
		if alias == nil {
			return nil, nil
		}

		ns, err := namespace.FromContext(ctx)
		if err != nil {
			return nil, err
		}
		if ns.ID != alias.NamespaceID {
			return logical.ErrorResponse("request and alias are in different namespaces"), logical.ErrPermissionDenied
		}

		// Fetch the associated entity
		entity, err := i.MemDBEntityByAliasIDInTxn(txn, alias.ID, true)
		if err != nil {
			return nil, err
		}

		// If there is no entity tied to a valid alias, something is wrong
		if entity == nil {
			return nil, fmt.Errorf("alias not associated to an entity")
		}

		aliases := []*identity.Alias{
			alias,
		}

		// Delete alias from the entity object
		err = i.deleteAliasesInEntityInTxn(txn, entity, aliases)
		if err != nil {
			return nil, err
		}

		// Update the entity index in the entities table
		err = i.MemDBUpsertEntityInTxn(txn, entity)
		if err != nil {
			return nil, err
		}

		switch alias.Local {
		case true:
			localAliases, err := i.parseLocalAliases(entity.ID)
			if err != nil {
				return nil, err
			}

			if localAliases == nil {
				return nil, nil
			}

			for i, item := range localAliases.Aliases {
				if item.ID == alias.ID {
					localAliases.Aliases = append(localAliases.Aliases[:i], localAliases.Aliases[i+1:]...)
					break
				}
			}

			marshaledAliases, err := ptypes.MarshalAny(localAliases)
			if err != nil {
				return nil, err
			}

			if err := i.localAliasPacker.PutItem(ctx, &storagepacker.Item{
				ID:      entity.ID,
				Message: marshaledAliases,
			}); err != nil {
				return nil, err
			}
		default:
			if err := i.persistEntity(ctx, entity); err != nil {
				return nil, err
			}
		}

		// Committing the transaction *after* successfully updating entity in
		// storage
		txn.Commit()

		return nil, nil
	}
}

// pathAliasIDList lists the IDs of all the valid aliases in the identity
// store
func (i *IdentityStore) pathAliasIDList() framework.OperationFunc {
	return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
		return i.handleAliasListCommon(ctx, false)
	}
}

var aliasHelp = map[string][2]string{
	"alias": {
		"Create a new alias.",
		"",
	},
	"alias-id": {
		"Update, read or delete an alias ID.",
		"",
	},
	"alias-id-list": {
		"List all the alias IDs.",
		"",
	},
}
