// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package vault

import (
	"context"
	"fmt"
	"strings"

	"github.com/golang/protobuf/ptypes"
	"github.com/hashicorp/vault/helper/identity"
	"github.com/hashicorp/vault/helper/namespace"
	"github.com/hashicorp/vault/sdk/framework"
	"github.com/hashicorp/vault/sdk/logical"
)

func groupAliasPaths(i *IdentityStore) []*framework.Path {
	return []*framework.Path{
		{
			Pattern: "group-alias$",

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

			Fields: map[string]*framework.FieldSchema{
				"id": {
					Type:        framework.TypeString,
					Description: "ID of the group alias.",
				},
				"name": {
					Type:        framework.TypeString,
					Description: "Alias of the group.",
				},
				"mount_accessor": {
					Type:        framework.TypeString,
					Description: "Mount accessor to which this alias belongs to.",
				},
				"canonical_id": {
					Type:        framework.TypeString,
					Description: "ID of the group to which this is an alias.",
				},
			},

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

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

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

			Fields: map[string]*framework.FieldSchema{
				"id": {
					Type:        framework.TypeString,
					Description: "ID of the group alias.",
				},
				"name": {
					Type:        framework.TypeString,
					Description: "Alias of the group.",
				},
				"mount_accessor": {
					Type:        framework.TypeString,
					Description: "Mount accessor to which this alias belongs to.",
				},
				"canonical_id": {
					Type:        framework.TypeString,
					Description: "ID of the group to which this is an alias.",
				},
			},

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

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

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

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

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

func (i *IdentityStore) pathGroupAliasRegister() framework.OperationFunc {
	return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
		_, ok := d.GetOk("id")
		if ok {
			return i.pathGroupAliasIDUpdate()(ctx, req, d)
		}

		i.groupLock.Lock()
		defer i.groupLock.Unlock()

		return i.handleGroupAliasUpdateCommon(ctx, req, d, nil)
	}
}

func (i *IdentityStore) pathGroupAliasIDUpdate() framework.OperationFunc {
	return func(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
		groupAliasID := d.Get("id").(string)
		if groupAliasID == "" {
			return logical.ErrorResponse("empty group alias ID"), nil
		}

		i.groupLock.Lock()
		defer i.groupLock.Unlock()

		groupAlias, err := i.MemDBAliasByID(groupAliasID, true, true)
		if err != nil {
			return nil, err
		}
		if groupAlias == nil {
			return logical.ErrorResponse("invalid group alias ID"), nil
		}

		return i.handleGroupAliasUpdateCommon(ctx, req, d, groupAlias)
	}
}

// NOTE: Currently we don't allow by-factors modification of group aliases the
// way we do with entities. As a result if a groupAlias is defined here we know
// that this is an update, where they provided an ID parameter.
func (i *IdentityStore) handleGroupAliasUpdateCommon(ctx context.Context, req *logical.Request, d *framework.FieldData, groupAlias *identity.Alias) (*logical.Response, error) {
	var newGroup, previousGroup *identity.Group

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

	if groupAlias == nil {
		groupAlias = &identity.Alias{
			CreationTime: ptypes.TimestampNow(),
			NamespaceID:  ns.ID,
		}
		groupAlias.LastUpdateTime = groupAlias.CreationTime
	} else {
		if ns.ID != groupAlias.NamespaceID {
			return logical.ErrorResponse("existing alias not in the same namespace as request"), logical.ErrPermissionDenied
		}
		groupAlias.LastUpdateTime = ptypes.TimestampNow()
		if groupAlias.CreationTime == nil {
			groupAlias.CreationTime = groupAlias.LastUpdateTime
		}
	}

	// Get group alias name
	name := d.Get("name").(string)
	if name == "" {
		return logical.ErrorResponse("missing alias name"), nil
	}

	mountAccessor := d.Get("mount_accessor").(string)
	if mountAccessor == "" {
		return logical.ErrorResponse("missing mount_accessor"), nil
	}

	canonicalID := d.Get("canonical_id").(string)

	if groupAlias.Name == name && groupAlias.MountAccessor == mountAccessor && (canonicalID == "" || groupAlias.CanonicalID == canonicalID) {
		// Nothing to do, be idempotent
		return nil, nil
	}

	// Explicitly correct for previous versions that persisted this
	groupAlias.MountType = ""

	// Canonical ID handling
	{
		if canonicalID != "" {
			newGroup, err = i.MemDBGroupByID(canonicalID, true)
			if err != nil {
				return nil, err
			}
			if newGroup == nil {
				return logical.ErrorResponse("invalid group ID given in 'canonical_id'"), nil
			}
			if newGroup.Type != groupTypeExternal {
				return logical.ErrorResponse("alias can't be set on an internal group"), nil
			}
			if newGroup.NamespaceID != groupAlias.NamespaceID {
				return logical.ErrorResponse("group referenced with 'canonical_id' not in the same namespace as alias"), logical.ErrPermissionDenied
			}
			groupAlias.CanonicalID = canonicalID
		}
	}

	// Validate name/accessor whether new or update
	{
		mountEntry := i.router.MatchingMountByAccessor(mountAccessor)
		if mountEntry == nil {
			return logical.ErrorResponse(fmt.Sprintf("invalid mount accessor %q", mountAccessor)), nil
		}
		if mountEntry.Local {
			return logical.ErrorResponse(fmt.Sprintf("mount accessor %q is a local mount", mountAccessor)), nil
		}
		if mountEntry.NamespaceID != groupAlias.NamespaceID {
			return logical.ErrorResponse("mount referenced via 'mount_accessor' not in the same namespace as alias"), logical.ErrPermissionDenied
		}

		groupAliasByFactors, err := i.MemDBAliasByFactors(mountEntry.Accessor, name, false, true)
		if err != nil {
			return nil, err
		}
		// This check will still work for the new case too since it won't have
		// an ID yet
		if groupAliasByFactors != nil && groupAliasByFactors.ID != groupAlias.ID {
			return logical.ErrorResponse("combination of mount and group alias name is already in use"), nil
		}

		groupAlias.Name = name
		groupAlias.MountAccessor = mountAccessor
	}

	switch groupAlias.ID {
	case "":
		// It's a new alias
		if newGroup == nil {
			// If this is a new alias being tied to a non-existent group,
			// create a new group for it
			newGroup = &identity.Group{
				Type: groupTypeExternal,
			}
		}

	default:
		// Fetch the group, if any, to which the alias is tied to
		previousGroup, err = i.MemDBGroupByAliasID(groupAlias.ID, true)
		if err != nil {
			return nil, err
		}
		if previousGroup == nil {
			return nil, fmt.Errorf("group alias is not associated with a group")
		}
		if previousGroup.NamespaceID != groupAlias.NamespaceID {
			return logical.ErrorResponse("previous group found for alias not in the same namespace as alias"), logical.ErrPermissionDenied
		}

		if newGroup == nil || newGroup.ID == previousGroup.ID {
			// If newGroup is nil they didn't specify a canonical ID, so they
			// aren't trying to update it; set the existing group as the "new"
			// one. If it's the same ID they specified the same canonical ID,
			// so follow the same behavior.
			newGroup = previousGroup
			previousGroup = nil
		} else {
			// The alias is moving, so nil out the previous group alias
			previousGroup.Alias = nil
		}
	}

	newGroup.Alias = groupAlias
	err = i.sanitizeAndUpsertGroup(ctx, newGroup, previousGroup, nil)
	if err != nil {
		return nil, err
	}

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

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

		groupAlias, err := i.MemDBAliasByID(groupAliasID, false, true)
		if err != nil {
			return nil, err
		}

		return i.handleAliasReadCommon(ctx, groupAlias)
	}
}

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

		i.groupLock.Lock()
		defer i.groupLock.Unlock()

		txn := i.db.Txn(true)
		defer txn.Abort()

		alias, err := i.MemDBAliasByIDInTxn(txn, groupAliasID, false, true)
		if err != nil {
			return nil, err
		}

		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 namespace is not the same as the group alias namespace"), logical.ErrPermissionDenied
		}

		group, err := i.MemDBGroupByAliasIDInTxn(txn, alias.ID, true)
		if err != nil {
			return nil, err
		}

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

		// Delete group alias in memdb
		err = i.MemDBDeleteAliasByIDInTxn(txn, group.Alias.ID, true)
		if err != nil {
			return nil, err
		}

		// Delete the alias
		group.Alias = nil

		err = i.UpsertGroupInTxn(ctx, txn, group, true)
		if err != nil {
			return nil, err
		}

		txn.Commit()

		return nil, nil
	}
}

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

var groupAliasHelp = map[string][2]string{
	"group-alias": {
		"Creates a new group alias, or updates an existing one.",
		"",
	},
	"group-alias-id": {
		"Update, read or delete a group alias using ID.",
		"",
	},
	"group-alias-id-list": {
		"List all the group alias IDs.",
		"",
	},
}
