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

package pki

import (
	"context"
	"net/http"

	"github.com/hashicorp/vault/builtin/logical/pki/issuing"
	"github.com/hashicorp/vault/sdk/framework"
	"github.com/hashicorp/vault/sdk/logical"
)

func pathConfigCA(b *backend) *framework.Path {
	return &framework.Path{
		Pattern: "config/ca",

		DisplayAttrs: &framework.DisplayAttributes{
			OperationPrefix: operationPrefixPKI,
			OperationVerb:   "configure",
			OperationSuffix: "ca",
		},

		Fields: map[string]*framework.FieldSchema{
			"pem_bundle": {
				Type: framework.TypeString,
				Description: `PEM-format, concatenated unencrypted
secret key and certificate.`,
			},
		},

		Operations: map[logical.Operation]framework.OperationHandler{
			logical.UpdateOperation: &framework.PathOperation{
				Callback: b.pathImportIssuers,
				Responses: map[int][]framework.Response{
					http.StatusOK: {{
						Description: "OK",
						Fields: map[string]*framework.FieldSchema{
							"mapping": {
								Type:        framework.TypeMap,
								Description: "A mapping of issuer_id to key_id for all issuers included in this request",
								Required:    true,
							},
							"imported_keys": {
								Type:        framework.TypeCommaStringSlice,
								Description: "Net-new keys imported as a part of this request",
								Required:    true,
							},
							"imported_issuers": {
								Type:        framework.TypeCommaStringSlice,
								Description: "Net-new issuers imported as a part of this request",
								Required:    true,
							},
							"existing_keys": {
								Type:        framework.TypeCommaStringSlice,
								Description: "Existing keys specified as part of the import bundle of this request",
								Required:    true,
							},
							"existing_issuers": {
								Type:        framework.TypeCommaStringSlice,
								Description: "Existing issuers specified as part of the import bundle of this request",
								Required:    true,
							},
						},
					}},
				},
				// Read more about why these flags are set in backend.go.
				ForwardPerformanceStandby:   true,
				ForwardPerformanceSecondary: true,
			},
		},

		HelpSynopsis:    pathConfigCAHelpSyn,
		HelpDescription: pathConfigCAHelpDesc,
	}
}

const pathConfigCAHelpSyn = `
Set the CA certificate and private key used for generated credentials.
`

const pathConfigCAHelpDesc = `
This sets the CA information used for credentials generated by this
by this mount. This must be a PEM-format, concatenated unencrypted
secret key and certificate.

For security reasons, the secret key cannot be retrieved later.
`

func pathConfigIssuers(b *backend) *framework.Path {
	return &framework.Path{
		Pattern: "config/issuers",

		DisplayAttrs: &framework.DisplayAttributes{
			OperationPrefix: operationPrefixPKI,
		},

		Fields: map[string]*framework.FieldSchema{
			defaultRef: {
				Type:        framework.TypeString,
				Description: `Reference (name or identifier) to the default issuer.`,
			},
			"default_follows_latest_issuer": {
				Type:        framework.TypeBool,
				Description: `Whether the default issuer should automatically follow the latest generated or imported issuer. Defaults to false.`,
				Default:     false,
			},
		},
		Operations: map[logical.Operation]framework.OperationHandler{
			logical.ReadOperation: &framework.PathOperation{
				Callback: b.pathCAIssuersRead,
				DisplayAttrs: &framework.DisplayAttributes{
					OperationSuffix: "issuers-configuration",
				},
				Responses: map[int][]framework.Response{
					http.StatusOK: {{
						Description: "OK",
						Fields: map[string]*framework.FieldSchema{
							"default": {
								Type:        framework.TypeString,
								Description: `Reference (name or identifier) to the default issuer.`,
								Required:    true,
							},
							"default_follows_latest_issuer": {
								Type:        framework.TypeBool,
								Description: `Whether the default issuer should automatically follow the latest generated or imported issuer. Defaults to false.`,
								Required:    true,
							},
						},
					}},
				},
			},
			logical.UpdateOperation: &framework.PathOperation{
				Callback: b.pathCAIssuersWrite,
				DisplayAttrs: &framework.DisplayAttributes{
					OperationVerb:   "configure",
					OperationSuffix: "issuers",
				},
				Responses: map[int][]framework.Response{
					http.StatusOK: {{
						Description: "OK",
						Fields: map[string]*framework.FieldSchema{
							"default": {
								Type:        framework.TypeString,
								Description: `Reference (name or identifier) to the default issuer.`,
							},
							"default_follows_latest_issuer": {
								Type:        framework.TypeBool,
								Description: `Whether the default issuer should automatically follow the latest generated or imported issuer. Defaults to false.`,
							},
						},
					}},
				},
				// Read more about why these flags are set in backend.go.
				ForwardPerformanceStandby:   true,
				ForwardPerformanceSecondary: true,
			},
		},

		HelpSynopsis:    pathConfigIssuersHelpSyn,
		HelpDescription: pathConfigIssuersHelpDesc,
	}
}

func pathReplaceRoot(b *backend) *framework.Path {
	return &framework.Path{
		Pattern: "root/replace",

		DisplayAttrs: &framework.DisplayAttributes{
			OperationPrefix: operationPrefixPKI,
			OperationVerb:   "replace",
			OperationSuffix: "root",
		},

		Fields: map[string]*framework.FieldSchema{
			"default": {
				Type:        framework.TypeString,
				Description: `Reference (name or identifier) to the default issuer.`,
				Default:     "next",
			},
		},

		Operations: map[logical.Operation]framework.OperationHandler{
			logical.UpdateOperation: &framework.PathOperation{
				Callback: b.pathCAIssuersWrite,
				Responses: map[int][]framework.Response{
					http.StatusOK: {{
						Description: "OK",
						Fields: map[string]*framework.FieldSchema{
							"default": {
								Type:        framework.TypeString,
								Description: `Reference (name or identifier) to the default issuer.`,
								Required:    true,
							},
							"default_follows_latest_issuer": {
								Type:        framework.TypeBool,
								Description: `Whether the default issuer should automatically follow the latest generated or imported issuer. Defaults to false.`,
								Required:    true,
							},
						},
					}},
				},
				// Read more about why these flags are set in backend.go.
				ForwardPerformanceStandby:   true,
				ForwardPerformanceSecondary: true,
			},
		},

		HelpSynopsis:    pathConfigIssuersHelpSyn,
		HelpDescription: pathConfigIssuersHelpDesc,
	}
}

func (b *backend) pathCAIssuersRead(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
	if b.UseLegacyBundleCaStorage() {
		return logical.ErrorResponse("Cannot read defaults until migration has completed"), nil
	}

	sc := b.makeStorageContext(ctx, req.Storage)
	config, err := sc.getIssuersConfig()
	if err != nil {
		return logical.ErrorResponse("Error loading issuers configuration: " + err.Error()), nil
	}

	return b.formatCAIssuerConfigRead(config), nil
}

func (b *backend) formatCAIssuerConfigRead(config *issuing.IssuerConfigEntry) *logical.Response {
	return &logical.Response{
		Data: map[string]interface{}{
			defaultRef:                      config.DefaultIssuerId,
			"default_follows_latest_issuer": config.DefaultFollowsLatestIssuer,
		},
	}
}

func (b *backend) pathCAIssuersWrite(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
	// Since we're planning on updating issuers here, grab the lock so we've
	// got a consistent view.
	b.issuersLock.Lock()
	defer b.issuersLock.Unlock()

	if b.UseLegacyBundleCaStorage() {
		return logical.ErrorResponse("Cannot update defaults until migration has completed"), nil
	}

	sc := b.makeStorageContext(ctx, req.Storage)

	// Validate the new default reference.
	newDefault := data.Get(defaultRef).(string)
	if len(newDefault) == 0 || newDefault == defaultRef {
		return logical.ErrorResponse("Invalid issuer specification; must be non-empty and can't be 'default'."), nil
	}
	parsedIssuer, err := sc.resolveIssuerReference(newDefault)
	if err != nil {
		return logical.ErrorResponse("Error resolving issuer reference: " + err.Error()), nil
	}
	entry, err := sc.fetchIssuerById(parsedIssuer)
	if err != nil {
		return logical.ErrorResponse("Unable to fetch issuer: " + err.Error()), nil
	}

	// Get the other new parameters. This doesn't exist on the /root/replace
	// variant of this call.
	var followIssuer bool
	followIssuersRaw, followOk := data.GetOk("default_follows_latest_issuer")
	if followOk {
		followIssuer = followIssuersRaw.(bool)
	}

	// Update the config
	config, err := sc.getIssuersConfig()
	if err != nil {
		return logical.ErrorResponse("Unable to fetch existing issuers configuration: " + err.Error()), nil
	}
	config.DefaultIssuerId = parsedIssuer
	if followOk {
		config.DefaultFollowsLatestIssuer = followIssuer
	}

	// Add our warning if necessary.
	response := b.formatCAIssuerConfigRead(config)
	if len(entry.KeyID) == 0 {
		msg := "This selected default issuer has no key associated with it. Some operations like issuing certificates and signing CRLs will be unavailable with the requested default issuer until a key is imported or the default issuer is changed."
		response.AddWarning(msg)
		b.Logger().Error(msg)
	}

	if err := sc.setIssuersConfig(config); err != nil {
		return logical.ErrorResponse("Error updating issuer configuration: " + err.Error()), nil
	}

	return response, nil
}

const pathConfigIssuersHelpSyn = `Read and set the default issuer certificate for signing.`

const pathConfigIssuersHelpDesc = `
This path allows configuration of issuer parameters.

Presently, the "default" parameter controls which issuer is the default,
accessible by the existing signing paths (/root/sign-intermediate,
/root/sign-self-issued, /sign-verbatim, /sign/:role, and /issue/:role).

The /root/replace path is aliased to this path, with default taking the
value of the issuer with the name "next", if it exists.
`

func pathConfigKeys(b *backend) *framework.Path {
	return &framework.Path{
		Pattern: "config/keys",

		DisplayAttrs: &framework.DisplayAttributes{
			OperationPrefix: operationPrefixPKI,
		},

		Fields: map[string]*framework.FieldSchema{
			defaultRef: {
				Type:        framework.TypeString,
				Description: `Reference (name or identifier) of the default key.`,
			},
		},

		Operations: map[logical.Operation]framework.OperationHandler{
			logical.UpdateOperation: &framework.PathOperation{
				Callback: b.pathKeyDefaultWrite,
				DisplayAttrs: &framework.DisplayAttributes{
					OperationVerb:   "configure",
					OperationSuffix: "keys",
				},
				Responses: map[int][]framework.Response{
					http.StatusOK: {{
						Description: "OK",
						Fields: map[string]*framework.FieldSchema{
							"default": {
								Type:        framework.TypeString,
								Description: `Reference (name or identifier) to the default issuer.`,
								Required:    true,
							},
						},
					}},
				},
				ForwardPerformanceStandby:   true,
				ForwardPerformanceSecondary: true,
			},
			logical.ReadOperation: &framework.PathOperation{
				Callback: b.pathKeyDefaultRead,
				DisplayAttrs: &framework.DisplayAttributes{
					OperationSuffix: "keys-configuration",
				},
				Responses: map[int][]framework.Response{
					http.StatusOK: {{
						Description: "OK",
						Fields: map[string]*framework.FieldSchema{
							"default": {
								Type:        framework.TypeString,
								Description: `Reference (name or identifier) to the default issuer.`,
							},
						},
					}},
				},
				ForwardPerformanceStandby:   false,
				ForwardPerformanceSecondary: false,
			},
		},

		HelpSynopsis:    pathConfigKeysHelpSyn,
		HelpDescription: pathConfigKeysHelpDesc,
	}
}

func (b *backend) pathKeyDefaultRead(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
	if b.UseLegacyBundleCaStorage() {
		return logical.ErrorResponse("Cannot read key defaults until migration has completed"), nil
	}

	sc := b.makeStorageContext(ctx, req.Storage)
	config, err := sc.getKeysConfig()
	if err != nil {
		return logical.ErrorResponse("Error loading keys configuration: " + err.Error()), nil
	}

	return &logical.Response{
		Data: map[string]interface{}{
			defaultRef: config.DefaultKeyId,
		},
	}, nil
}

func (b *backend) pathKeyDefaultWrite(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
	// Since we're planning on updating keys here, grab the lock so we've
	// got a consistent view.
	b.issuersLock.Lock()
	defer b.issuersLock.Unlock()

	if b.UseLegacyBundleCaStorage() {
		return logical.ErrorResponse("Cannot update key defaults until migration has completed"), nil
	}

	newDefault := data.Get(defaultRef).(string)
	if len(newDefault) == 0 || newDefault == defaultRef {
		return logical.ErrorResponse("Invalid key specification; must be non-empty and can't be 'default'."), nil
	}

	sc := b.makeStorageContext(ctx, req.Storage)
	parsedKey, err := sc.resolveKeyReference(newDefault)
	if err != nil {
		return logical.ErrorResponse("Error resolving issuer reference: " + err.Error()), nil
	}

	err = sc.updateDefaultKeyId(parsedKey)
	if err != nil {
		return logical.ErrorResponse("Error updating issuer configuration: " + err.Error()), nil
	}

	return &logical.Response{
		Data: map[string]interface{}{
			defaultRef: parsedKey,
		},
	}, nil
}

const pathConfigKeysHelpSyn = `Read and set the default key used for signing`

const pathConfigKeysHelpDesc = `
This path allows configuration of key parameters.

The "default" parameter controls which key is the default used by signing paths.
`
