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

package authmetadata

/*
	authmetadata is a package offering convenience and
	standardization when supporting an `auth_metadata`
	field in a plugin's configuration. This then controls
	what metadata is added to an Auth during login.

	To see an example of how to add and use it, check out
	how these structs and fields are used in the AWS auth
	method.

	Or, check out its acceptance test in this package to
	see its integration points.
*/

import (
	"encoding/json"
	"errors"
	"fmt"
	"strings"

	"github.com/hashicorp/go-secure-stdlib/strutil"
	"github.com/hashicorp/vault/sdk/framework"
	"github.com/hashicorp/vault/sdk/logical"
)

// Fields is for configuring a back-end's available
// default and additional fields. These are used for
// providing a verbose field description, and for parsing
// user input.
type Fields struct {
	// The field name as it'll be reflected in the user-facing
	// schema.
	FieldName string

	// Default is a list of the default fields that should
	// be included if a user sends "default" in their list
	// of desired fields. These fields should all have a
	// low rate of change because each change can incur a
	// write to storage.
	Default []string

	// AvailableToAdd is a list of fields not included by
	// default, that the user may include.
	AvailableToAdd []string
}

func (f *Fields) all() []string {
	return append(f.Default, f.AvailableToAdd...)
}

// FieldSchema takes the default and additionally available
// fields, and uses them to generate a verbose description
// regarding how to use the "auth_metadata" field.
func FieldSchema(fields *Fields) *framework.FieldSchema {
	return &framework.FieldSchema{
		Type:        framework.TypeCommaStringSlice,
		Description: description(fields),
		DisplayAttrs: &framework.DisplayAttributes{
			Name:  fields.FieldName,
			Value: "field1,field2",
		},
		Default: []string{"default"},
	}
}

func NewHandler(fields *Fields) *Handler {
	return &Handler{
		fields: fields,
	}
}

type Handler struct {
	// authMetadata is an explicit list of all the user's configured
	// fields that are being added to auth metadata. If it is set to
	// default or unconfigured, it will be nil. Otherwise, it will
	// hold the explicit fields set by the user.
	authMetadata []string

	// fields is a list of the configured default and available
	// fields.
	fields *Fields
}

// AuthMetadata is intended to be used on config reads.
// It gets an explicit list of all the user's configured
// fields that are being added to auth metadata.
func (h *Handler) AuthMetadata() []string {
	if h.authMetadata == nil {
		return h.fields.Default
	}
	return h.authMetadata
}

// ParseAuthMetadata is intended to be used on config create/update.
// It takes a user's selected fields (or lack thereof),
// converts it to a list of explicit fields, and adds it to the Handler
// for later storage.
func (h *Handler) ParseAuthMetadata(data *framework.FieldData) error {
	userProvidedRaw, ok := data.GetOk(h.fields.FieldName)
	if !ok {
		// Nothing further to do here.
		return nil
	}
	userProvided, ok := userProvidedRaw.([]string)
	if !ok {
		return fmt.Errorf("%s is an unexpected type of %T", userProvidedRaw, userProvidedRaw)
	}
	userProvided = strutil.RemoveDuplicates(userProvided, true)

	// If the only field the user has chosen was the default field,
	// we don't store anything so we won't have to do a storage
	// migration if the default changes.
	if len(userProvided) == 1 && userProvided[0] == "default" {
		h.authMetadata = nil
		return nil
	}

	// Validate and store the input.
	if strutil.StrListContains(userProvided, "default") {
		return fmt.Errorf("%q contains default - default can't be used in combination with other fields",
			userProvided)
	}
	if !strutil.StrListSubset(h.fields.all(), userProvided) {
		return fmt.Errorf("%q contains an unavailable field, please select from %q",
			strings.Join(userProvided, ", "), strings.Join(h.fields.all(), ", "))
	}
	h.authMetadata = userProvided
	return nil
}

// PopulateDesiredMetadata is intended to be used during login
// just before returning an auth.
// It takes the available auth metadata and,
// if the auth should have it, adds it to the auth's metadata.
func (h *Handler) PopulateDesiredMetadata(auth *logical.Auth, available map[string]string) error {
	if auth == nil {
		return errors.New("auth is nil")
	}
	if auth.Metadata == nil {
		auth.Metadata = make(map[string]string)
	}
	if auth.Alias == nil {
		auth.Alias = &logical.Alias{}
	}
	if auth.Alias.Metadata == nil {
		auth.Alias.Metadata = make(map[string]string)
	}
	fieldsToInclude := h.fields.Default
	if h.authMetadata != nil {
		fieldsToInclude = h.authMetadata
	}
	for availableField, itsValue := range available {
		if itsValue == "" {
			// Don't bother setting fields for which there is no value.
			continue
		}
		if strutil.StrListContains(fieldsToInclude, availableField) {
			auth.Metadata[availableField] = itsValue
			auth.Alias.Metadata[availableField] = itsValue
		}
	}
	return nil
}

func (h *Handler) MarshalJSON() ([]byte, error) {
	return json.Marshal(&struct {
		AuthMetadata []string `json:"auth_metadata"`
	}{
		AuthMetadata: h.authMetadata,
	})
}

func (h *Handler) UnmarshalJSON(data []byte) error {
	jsonable := &struct {
		AuthMetadata []string `json:"auth_metadata"`
	}{
		AuthMetadata: h.authMetadata,
	}
	if err := json.Unmarshal(data, jsonable); err != nil {
		return err
	}
	h.authMetadata = jsonable.AuthMetadata
	return nil
}

func description(fields *Fields) string {
	desc := "The metadata to include on the aliases and audit logs generated by this plugin."
	if len(fields.Default) > 0 {
		desc += fmt.Sprintf(" When set to 'default', includes: %s.", strings.Join(fields.Default, ", "))
	}
	if len(fields.AvailableToAdd) > 0 {
		desc += fmt.Sprintf(" These fields are available to add: %s.", strings.Join(fields.AvailableToAdd, ", "))
	}
	desc += " Not editing this field means the 'default' fields are included." +
		" Explicitly setting this field to empty overrides the 'default' and means no metadata will be included." +
		" If not using 'default', explicit fields must be sent like: 'field1,field2'."
	return desc
}
