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

package pki

import (
	"context"
	"fmt"
	"net/http"

	"github.com/hashicorp/go-secure-stdlib/parseutil"
	"github.com/hashicorp/vault/helper/constants"
	"github.com/hashicorp/vault/sdk/framework"
	"github.com/hashicorp/vault/sdk/helper/errutil"
	"github.com/hashicorp/vault/sdk/logical"
)

const latestCrlConfigVersion = 1

// CRLConfig holds basic CRL configuration information
type crlConfig struct {
	Version                   int    `json:"version"`
	Expiry                    string `json:"expiry"`
	Disable                   bool   `json:"disable"`
	OcspDisable               bool   `json:"ocsp_disable"`
	AutoRebuild               bool   `json:"auto_rebuild"`
	AutoRebuildGracePeriod    string `json:"auto_rebuild_grace_period"`
	OcspExpiry                string `json:"ocsp_expiry"`
	EnableDelta               bool   `json:"enable_delta"`
	DeltaRebuildInterval      string `json:"delta_rebuild_interval"`
	UseGlobalQueue            bool   `json:"cross_cluster_revocation"`
	UnifiedCRL                bool   `json:"unified_crl"`
	UnifiedCRLOnExistingPaths bool   `json:"unified_crl_on_existing_paths"`
}

// Implicit default values for the config if it does not exist.
var defaultCrlConfig = crlConfig{
	Version:                   latestCrlConfigVersion,
	Expiry:                    "72h",
	Disable:                   false,
	OcspDisable:               false,
	OcspExpiry:                "12h",
	AutoRebuild:               false,
	AutoRebuildGracePeriod:    "12h",
	EnableDelta:               false,
	DeltaRebuildInterval:      "15m",
	UseGlobalQueue:            false,
	UnifiedCRL:                false,
	UnifiedCRLOnExistingPaths: false,
}

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

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

		Fields: map[string]*framework.FieldSchema{
			"expiry": {
				Type: framework.TypeString,
				Description: `The amount of time the generated CRL should be
valid; defaults to 72 hours`,
				Default: "72h",
			},
			"disable": {
				Type:        framework.TypeBool,
				Description: `If set to true, disables generating the CRL entirely.`,
			},
			"ocsp_disable": {
				Type:        framework.TypeBool,
				Description: `If set to true, ocsp unauthorized responses will be returned.`,
			},
			"ocsp_expiry": {
				Type: framework.TypeString,
				Description: `The amount of time an OCSP response will be valid (controls 
the NextUpdate field); defaults to 12 hours`,
				Default: "1h",
			},
			"auto_rebuild": {
				Type:        framework.TypeBool,
				Description: `If set to true, enables automatic rebuilding of the CRL`,
			},
			"auto_rebuild_grace_period": {
				Type:        framework.TypeString,
				Description: `The time before the CRL expires to automatically rebuild it, when enabled. Must be shorter than the CRL expiry. Defaults to 12h.`,
				Default:     "12h",
			},
			"enable_delta": {
				Type:        framework.TypeBool,
				Description: `Whether to enable delta CRLs between authoritative CRL rebuilds`,
			},
			"delta_rebuild_interval": {
				Type:        framework.TypeString,
				Description: `The time between delta CRL rebuilds if a new revocation has occurred. Must be shorter than the CRL expiry. Defaults to 15m.`,
				Default:     "15m",
			},
			"cross_cluster_revocation": {
				Type: framework.TypeBool,
				Description: `Whether to enable a global, cross-cluster revocation queue.
Must be used with auto_rebuild=true.`,
			},
			"unified_crl": {
				Type: framework.TypeBool,
				Description: `If set to true enables global replication of revocation entries,
also enabling unified versions of OCSP and CRLs if their respective features are enabled.
disable for CRLs and ocsp_disable for OCSP.`,
				Default: "false",
			},
			"unified_crl_on_existing_paths": {
				Type: framework.TypeBool,
				Description: `If set to true, 
existing CRL and OCSP paths will return the unified CRL instead of a response based on cluster-local data`,
				Default: "false",
			},
		},

		Operations: map[logical.Operation]framework.OperationHandler{
			logical.ReadOperation: &framework.PathOperation{
				DisplayAttrs: &framework.DisplayAttributes{
					OperationSuffix: "crl-configuration",
				},
				Callback: b.pathCRLRead,
				Responses: map[int][]framework.Response{
					http.StatusOK: {{
						Description: "OK",
						Fields: map[string]*framework.FieldSchema{
							"expiry": {
								Type: framework.TypeString,
								Description: `The amount of time the generated CRL should be
valid; defaults to 72 hours`,
								Required: true,
							},
							"disable": {
								Type:        framework.TypeBool,
								Description: `If set to true, disables generating the CRL entirely.`,
								Required:    true,
							},
							"ocsp_disable": {
								Type:        framework.TypeBool,
								Description: `If set to true, ocsp unauthorized responses will be returned.`,
								Required:    true,
							},
							"ocsp_expiry": {
								Type: framework.TypeString,
								Description: `The amount of time an OCSP response will be valid (controls 
the NextUpdate field); defaults to 12 hours`,
								Required: true,
							},
							"auto_rebuild": {
								Type:        framework.TypeBool,
								Description: `If set to true, enables automatic rebuilding of the CRL`,
								Required:    true,
							},
							"auto_rebuild_grace_period": {
								Type:        framework.TypeString,
								Description: `The time before the CRL expires to automatically rebuild it, when enabled. Must be shorter than the CRL expiry. Defaults to 12h.`,
								Required:    true,
							},
							"enable_delta": {
								Type:        framework.TypeBool,
								Description: `Whether to enable delta CRLs between authoritative CRL rebuilds`,
								Required:    true,
							},
							"delta_rebuild_interval": {
								Type:        framework.TypeString,
								Description: `The time between delta CRL rebuilds if a new revocation has occurred. Must be shorter than the CRL expiry. Defaults to 15m.`,
								Required:    true,
							},
							"cross_cluster_revocation": {
								Type: framework.TypeBool,
								Description: `Whether to enable a global, cross-cluster revocation queue.
Must be used with auto_rebuild=true.`,
								Required: true,
							},
							"unified_crl": {
								Type: framework.TypeBool,
								Description: `If set to true enables global replication of revocation entries,
also enabling unified versions of OCSP and CRLs if their respective features are enabled.
disable for CRLs and ocsp_disable for OCSP.`,
								Required: true,
							},
							"unified_crl_on_existing_paths": {
								Type: framework.TypeBool,
								Description: `If set to true, 
existing CRL and OCSP paths will return the unified CRL instead of a response based on cluster-local data`,
								Required: true,
							},
						},
					}},
				},
			},
			logical.UpdateOperation: &framework.PathOperation{
				Callback: b.pathCRLWrite,
				DisplayAttrs: &framework.DisplayAttributes{
					OperationVerb:   "configure",
					OperationSuffix: "crl",
				},
				Responses: map[int][]framework.Response{
					http.StatusOK: {{
						Description: "OK",
						Fields: map[string]*framework.FieldSchema{
							"expiry": {
								Type: framework.TypeString,
								Description: `The amount of time the generated CRL should be
valid; defaults to 72 hours`,
								Default: "72h",
							},
							"disable": {
								Type:        framework.TypeBool,
								Description: `If set to true, disables generating the CRL entirely.`,
							},
							"ocsp_disable": {
								Type:        framework.TypeBool,
								Description: `If set to true, ocsp unauthorized responses will be returned.`,
							},
							"ocsp_expiry": {
								Type: framework.TypeString,
								Description: `The amount of time an OCSP response will be valid (controls 
the NextUpdate field); defaults to 12 hours`,
								Default: "1h",
							},
							"auto_rebuild": {
								Type:        framework.TypeBool,
								Description: `If set to true, enables automatic rebuilding of the CRL`,
							},
							"auto_rebuild_grace_period": {
								Type:        framework.TypeString,
								Description: `The time before the CRL expires to automatically rebuild it, when enabled. Must be shorter than the CRL expiry. Defaults to 12h.`,
								Default:     "12h",
							},
							"enable_delta": {
								Type:        framework.TypeBool,
								Description: `Whether to enable delta CRLs between authoritative CRL rebuilds`,
							},
							"delta_rebuild_interval": {
								Type:        framework.TypeString,
								Description: `The time between delta CRL rebuilds if a new revocation has occurred. Must be shorter than the CRL expiry. Defaults to 15m.`,
								Default:     "15m",
							},
							"cross_cluster_revocation": {
								Type: framework.TypeBool,
								Description: `Whether to enable a global, cross-cluster revocation queue.
Must be used with auto_rebuild=true.`,
								Required: false,
							},
							"unified_crl": {
								Type: framework.TypeBool,
								Description: `If set to true enables global replication of revocation entries,
also enabling unified versions of OCSP and CRLs if their respective features are enabled.
disable for CRLs and ocsp_disable for OCSP.`,
								Required: false,
							},
							"unified_crl_on_existing_paths": {
								Type: framework.TypeBool,
								Description: `If set to true, 
existing CRL and OCSP paths will return the unified CRL instead of a response based on cluster-local data`,
								Required: false,
							},
						},
					}},
				},
				// Read more about why these flags are set in backend.go.
				ForwardPerformanceStandby:   true,
				ForwardPerformanceSecondary: true,
			},
		},

		HelpSynopsis:    pathConfigCRLHelpSyn,
		HelpDescription: pathConfigCRLHelpDesc,
	}
}

func (b *backend) pathCRLRead(ctx context.Context, req *logical.Request, _ *framework.FieldData) (*logical.Response, error) {
	sc := b.makeStorageContext(ctx, req.Storage)
	config, err := sc.getRevocationConfig()
	if err != nil {
		return nil, err
	}

	return genResponseFromCrlConfig(config), nil
}

func (b *backend) pathCRLWrite(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
	sc := b.makeStorageContext(ctx, req.Storage)
	config, err := sc.getRevocationConfig()
	if err != nil {
		return nil, err
	}

	if expiryRaw, ok := d.GetOk("expiry"); ok {
		expiry := expiryRaw.(string)
		_, err := parseutil.ParseDurationSecond(expiry)
		if err != nil {
			return logical.ErrorResponse(fmt.Sprintf("given expiry could not be decoded: %s", err)), nil
		}
		config.Expiry = expiry
	}

	oldDisable := config.Disable
	if disableRaw, ok := d.GetOk("disable"); ok {
		config.Disable = disableRaw.(bool)
	}

	if ocspDisableRaw, ok := d.GetOk("ocsp_disable"); ok {
		config.OcspDisable = ocspDisableRaw.(bool)
	}

	if expiryRaw, ok := d.GetOk("ocsp_expiry"); ok {
		expiry := expiryRaw.(string)
		duration, err := parseutil.ParseDurationSecond(expiry)
		if err != nil {
			return logical.ErrorResponse(fmt.Sprintf("given ocsp_expiry could not be decoded: %s", err)), nil
		}
		if duration < 0 {
			return logical.ErrorResponse(fmt.Sprintf("ocsp_expiry must be greater than or equal to 0 got: %s", duration)), nil
		}
		config.OcspExpiry = expiry
	}

	oldAutoRebuild := config.AutoRebuild
	if autoRebuildRaw, ok := d.GetOk("auto_rebuild"); ok {
		config.AutoRebuild = autoRebuildRaw.(bool)
	}

	if autoRebuildGracePeriodRaw, ok := d.GetOk("auto_rebuild_grace_period"); ok {
		autoRebuildGracePeriod := autoRebuildGracePeriodRaw.(string)
		if _, err := parseutil.ParseDurationSecond(autoRebuildGracePeriod); err != nil {
			return logical.ErrorResponse(fmt.Sprintf("given auto_rebuild_grace_period could not be decoded: %s", err)), nil
		}
		config.AutoRebuildGracePeriod = autoRebuildGracePeriod
	}

	oldEnableDelta := config.EnableDelta
	if enableDeltaRaw, ok := d.GetOk("enable_delta"); ok {
		config.EnableDelta = enableDeltaRaw.(bool)
	}

	if deltaRebuildIntervalRaw, ok := d.GetOk("delta_rebuild_interval"); ok {
		deltaRebuildInterval := deltaRebuildIntervalRaw.(string)
		if _, err := parseutil.ParseDurationSecond(deltaRebuildInterval); err != nil {
			return logical.ErrorResponse(fmt.Sprintf("given delta_rebuild_interval could not be decoded: %s", err)), nil
		}
		config.DeltaRebuildInterval = deltaRebuildInterval
	}

	if useGlobalQueue, ok := d.GetOk("cross_cluster_revocation"); ok {
		config.UseGlobalQueue = useGlobalQueue.(bool)
	}

	oldUnifiedCRL := config.UnifiedCRL
	if unifiedCrlRaw, ok := d.GetOk("unified_crl"); ok {
		config.UnifiedCRL = unifiedCrlRaw.(bool)
	}

	if unifiedCrlOnExistingPathsRaw, ok := d.GetOk("unified_crl_on_existing_paths"); ok {
		config.UnifiedCRLOnExistingPaths = unifiedCrlOnExistingPathsRaw.(bool)
	}

	if config.UnifiedCRLOnExistingPaths && !config.UnifiedCRL {
		return logical.ErrorResponse("unified_crl_on_existing_paths cannot be enabled if unified_crl is disabled"), nil
	}

	expiry, _ := parseutil.ParseDurationSecond(config.Expiry)
	if config.AutoRebuild {
		gracePeriod, _ := parseutil.ParseDurationSecond(config.AutoRebuildGracePeriod)
		if gracePeriod >= expiry {
			return logical.ErrorResponse(fmt.Sprintf("CRL auto-rebuilding grace period (%v) must be strictly shorter than CRL expiry (%v) value when auto-rebuilding of CRLs is enabled", config.AutoRebuildGracePeriod, config.Expiry)), nil
		}
	}

	if config.EnableDelta {
		deltaRebuildInterval, _ := parseutil.ParseDurationSecond(config.DeltaRebuildInterval)
		if deltaRebuildInterval >= expiry {
			return logical.ErrorResponse(fmt.Sprintf("CRL delta rebuild window (%v) must be strictly shorter than CRL expiry (%v) value when delta CRLs are enabled", config.DeltaRebuildInterval, config.Expiry)), nil
		}
	}

	if !config.AutoRebuild {
		if config.EnableDelta {
			return logical.ErrorResponse("Delta CRLs cannot be enabled when auto rebuilding is disabled as the complete CRL is always regenerated!"), nil
		}

		if config.UseGlobalQueue {
			return logical.ErrorResponse("Global, cross-cluster revocation queue cannot be enabled when auto rebuilding is disabled as the local cluster may not have the certificate entry!"), nil
		}
	}

	if !constants.IsEnterprise && config.UseGlobalQueue {
		return logical.ErrorResponse("Global, cross-cluster revocation queue (cross_cluster_revocation) can only be enabled on Vault Enterprise."), nil
	}

	if !constants.IsEnterprise && config.UnifiedCRL {
		return logical.ErrorResponse("unified_crl can only be enabled on Vault Enterprise"), nil
	}

	isLocalMount := b.System().LocalMount()
	if isLocalMount && config.UseGlobalQueue {
		return logical.ErrorResponse("Global, cross-cluster revocation queue (cross_cluster_revocation) cannot be enabled on local mounts."),
			nil
	}

	if isLocalMount && config.UnifiedCRL {
		return logical.ErrorResponse("unified_crl cannot be enabled on local mounts."), nil
	}

	if !config.AutoRebuild && config.UnifiedCRL {
		return logical.ErrorResponse("unified_crl=true requires auto_rebuild=true, as unified CRLs cannot be rebuilt on every revocation."), nil
	}

	entry, err := logical.StorageEntryJSON("config/crl", config)
	if err != nil {
		return nil, err
	}
	err = req.Storage.Put(ctx, entry)
	if err != nil {
		return nil, err
	}

	b.crlBuilder.markConfigDirty()
	b.crlBuilder.reloadConfigIfRequired(sc)

	resp := genResponseFromCrlConfig(config)

	// Note this only affects/happens on the main cluster node, if you need to
	// notify something based on a configuration change on all server types
	// have a look at crlBuilder::reloadConfigIfRequired
	if oldDisable != config.Disable || (oldAutoRebuild && !config.AutoRebuild) || (oldEnableDelta != config.EnableDelta) || (oldUnifiedCRL != config.UnifiedCRL) {
		// It wasn't disabled but now it is (or equivalently, we were set to
		// auto-rebuild and we aren't now or equivalently, we changed our
		// mind about delta CRLs and need a new complete one or equivalently,
		// we changed our mind about unified CRLs), rotate the CRLs.
		warnings, crlErr := b.crlBuilder.rebuild(sc, true)
		if crlErr != nil {
			switch crlErr.(type) {
			case errutil.UserError:
				return logical.ErrorResponse(fmt.Sprintf("Error during CRL building: %s", crlErr)), nil
			default:
				return nil, fmt.Errorf("error encountered during CRL building: %w", crlErr)
			}
		}
		for index, warning := range warnings {
			resp.AddWarning(fmt.Sprintf("Warning %d during CRL rebuild: %v", index+1, warning))
		}
	}

	return resp, nil
}

func genResponseFromCrlConfig(config *crlConfig) *logical.Response {
	return &logical.Response{
		Data: map[string]interface{}{
			"expiry":                        config.Expiry,
			"disable":                       config.Disable,
			"ocsp_disable":                  config.OcspDisable,
			"ocsp_expiry":                   config.OcspExpiry,
			"auto_rebuild":                  config.AutoRebuild,
			"auto_rebuild_grace_period":     config.AutoRebuildGracePeriod,
			"enable_delta":                  config.EnableDelta,
			"delta_rebuild_interval":        config.DeltaRebuildInterval,
			"cross_cluster_revocation":      config.UseGlobalQueue,
			"unified_crl":                   config.UnifiedCRL,
			"unified_crl_on_existing_paths": config.UnifiedCRLOnExistingPaths,
		},
	}
}

const pathConfigCRLHelpSyn = `
Configure the CRL expiration.
`

const pathConfigCRLHelpDesc = `
This endpoint allows configuration of the CRL lifetime.
`
