// Copyright (c) Tailscale Inc & AUTHORS
// SPDX-License-Identifier: BSD-3-Clause

//go:build linux

package main

import (
	"bytes"
	"context"
	"crypto/tls"
	"crypto/x509"
	"encoding/base64"
	"encoding/json"
	"fmt"
	"io"
	"log"
	"net/http"
	"os"
	"path/filepath"
	"strings"
	"time"

	"tailscale.com/tailcfg"
	"tailscale.com/util/multierr"
)

// checkSecretPermissions checks the secret access permissions of the current
// pod. It returns an error if the basic permissions tailscale needs are
// missing, and reports whether the patch permission is additionally present.
//
// Errors encountered during the access checking process are logged, but ignored
// so that the pod tries to fail alive if the permissions exist and there's just
// something wrong with SelfSubjectAccessReviews. There shouldn't be, pods
// should always be able to use SSARs to assess their own permissions, but since
// we didn't use to check permissions this way we'll be cautious in case some
// old version of k8s deviates from the current behavior.
func checkSecretPermissions(ctx context.Context, secretName string) (canPatch bool, err error) {
	var errs []error
	for _, verb := range []string{"get", "update"} {
		ok, err := checkPermission(ctx, verb, secretName)
		if err != nil {
			log.Printf("error checking %s permission on secret %s: %v", verb, secretName, err)
		} else if !ok {
			errs = append(errs, fmt.Errorf("missing %s permission on secret %q", verb, secretName))
		}
	}
	if len(errs) > 0 {
		return false, multierr.New(errs...)
	}
	ok, err := checkPermission(ctx, "patch", secretName)
	if err != nil {
		log.Printf("error checking patch permission on secret %s: %v", secretName, err)
		return false, nil
	}
	return ok, nil
}

// checkPermission reports whether the current pod has permission to use the
// given verb (e.g. get, update, patch) on secretName.
func checkPermission(ctx context.Context, verb, secretName string) (bool, error) {
	sar := map[string]any{
		"apiVersion": "authorization.k8s.io/v1",
		"kind":       "SelfSubjectAccessReview",
		"spec": map[string]any{
			"resourceAttributes": map[string]any{
				"namespace": kubeNamespace,
				"verb":      verb,
				"resource":  "secrets",
				"name":      secretName,
			},
		},
	}
	bs, err := json.Marshal(sar)
	if err != nil {
		return false, err
	}
	req, err := http.NewRequest("POST", "/apis/authorization.k8s.io/v1/selfsubjectaccessreviews", bytes.NewReader(bs))
	if err != nil {
		return false, err
	}
	resp, err := doKubeRequest(ctx, req)
	if err != nil {
		return false, err
	}
	defer resp.Body.Close()
	bs, err = io.ReadAll(resp.Body)
	if err != nil {
		return false, err
	}
	var res struct {
		Status struct {
			Allowed bool `json:"allowed"`
		} `json:"status"`
	}
	if err := json.Unmarshal(bs, &res); err != nil {
		return false, err
	}
	return res.Status.Allowed, nil
}

// findKeyInKubeSecret inspects the kube secret secretName for a data
// field called "authkey", and returns its value if present.
func findKeyInKubeSecret(ctx context.Context, secretName string) (string, error) {
	req, err := http.NewRequest("GET", fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s", kubeNamespace, secretName), nil)
	if err != nil {
		return "", err
	}
	resp, err := doKubeRequest(ctx, req)
	if err != nil {
		if resp != nil && resp.StatusCode == http.StatusNotFound {
			// Kube secret doesn't exist yet, can't have an authkey.
			return "", nil
		}
		return "", err
	}
	defer resp.Body.Close()

	bs, err := io.ReadAll(resp.Body)
	if err != nil {
		return "", err
	}

	// We use a map[string]any here rather than import corev1.Secret,
	// because we only do very limited things to the secret, and
	// importing corev1 adds 12MiB to the compiled binary.
	var s map[string]any
	if err := json.Unmarshal(bs, &s); err != nil {
		return "", err
	}
	if d, ok := s["data"].(map[string]any); ok {
		if v, ok := d["authkey"].(string); ok {
			bs, err := base64.StdEncoding.DecodeString(v)
			if err != nil {
				return "", err
			}
			return string(bs), nil
		}
	}
	return "", nil
}

// storeDeviceInfo writes deviceID into the "device_id" data field of the kube
// secret secretName.
func storeDeviceInfo(ctx context.Context, secretName string, deviceID tailcfg.StableNodeID, fqdn string) error {
	// First check if the secret exists at all. Even if running on
	// kubernetes, we do not necessarily store state in a k8s secret.
	req, err := http.NewRequest("GET", fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s", kubeNamespace, secretName), nil)
	if err != nil {
		return err
	}
	resp, err := doKubeRequest(ctx, req)
	if err != nil {
		if resp != nil && resp.StatusCode >= 400 && resp.StatusCode <= 499 {
			// Assume the secret doesn't exist, or we don't have
			// permission to access it.
			return nil
		}
		return err
	}

	m := map[string]map[string]string{
		"stringData": {
			"device_id":   string(deviceID),
			"device_fqdn": fqdn,
		},
	}
	var b bytes.Buffer
	if err := json.NewEncoder(&b).Encode(m); err != nil {
		return err
	}
	req, err = http.NewRequest("PATCH", fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s?fieldManager=tailscale-container", kubeNamespace, secretName), &b)
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/strategic-merge-patch+json")
	if _, err := doKubeRequest(ctx, req); err != nil {
		return err
	}
	return nil
}

// deleteAuthKey deletes the 'authkey' field of the given kube
// secret. No-op if there is no authkey in the secret.
func deleteAuthKey(ctx context.Context, secretName string) error {
	// m is a JSON Patch data structure, see https://jsonpatch.com/ or RFC 6902.
	m := []struct {
		Op   string `json:"op"`
		Path string `json:"path"`
	}{
		{
			Op:   "remove",
			Path: "/data/authkey",
		},
	}
	var b bytes.Buffer
	if err := json.NewEncoder(&b).Encode(m); err != nil {
		return err
	}
	req, err := http.NewRequest("PATCH", fmt.Sprintf("/api/v1/namespaces/%s/secrets/%s?fieldManager=tailscale-container", kubeNamespace, secretName), &b)
	if err != nil {
		return err
	}
	req.Header.Set("Content-Type", "application/json-patch+json")
	if resp, err := doKubeRequest(ctx, req); err != nil {
		if resp != nil && resp.StatusCode == http.StatusUnprocessableEntity {
			// This is kubernetes-ese for "the field you asked to
			// delete already doesn't exist", aka no-op.
			return nil
		}
		return err
	}
	return nil
}

var (
	kubeHost      string
	kubeNamespace string
	kubeToken     string
	kubeHTTP      *http.Transport
)

func initKube(root string) {
	// If running in Kubernetes, set things up so that doKubeRequest
	// can talk successfully to the kube apiserver.
	if os.Getenv("KUBERNETES_SERVICE_HOST") == "" {
		return
	}

	kubeHost = os.Getenv("KUBERNETES_SERVICE_HOST") + ":" + os.Getenv("KUBERNETES_SERVICE_PORT_HTTPS")

	bs, err := os.ReadFile(filepath.Join(root, "var/run/secrets/kubernetes.io/serviceaccount/namespace"))
	if err != nil {
		log.Fatalf("Error reading kube namespace: %v", err)
	}
	kubeNamespace = strings.TrimSpace(string(bs))

	bs, err = os.ReadFile(filepath.Join(root, "var/run/secrets/kubernetes.io/serviceaccount/token"))
	if err != nil {
		log.Fatalf("Error reading kube token: %v", err)
	}
	kubeToken = strings.TrimSpace(string(bs))

	bs, err = os.ReadFile(filepath.Join(root, "var/run/secrets/kubernetes.io/serviceaccount/ca.crt"))
	if err != nil {
		log.Fatalf("Error reading kube CA cert: %v", err)
	}
	cp := x509.NewCertPool()
	cp.AppendCertsFromPEM(bs)
	kubeHTTP = &http.Transport{
		TLSClientConfig: &tls.Config{
			RootCAs: cp,
		},
		IdleConnTimeout: time.Second,
	}
}

// doKubeRequest sends r to the kube apiserver.
func doKubeRequest(ctx context.Context, r *http.Request) (*http.Response, error) {
	if kubeHTTP == nil {
		panic("not in kubernetes")
	}

	r.URL.Scheme = "https"
	r.URL.Host = kubeHost
	r.Header.Set("Authorization", "Bearer "+kubeToken)
	r.Header.Set("Accept", "application/json")

	resp, err := kubeHTTP.RoundTrip(r)
	if err != nil {
		return nil, err
	}
	if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated {
		return resp, fmt.Errorf("got non-200/201 status code %d", resp.StatusCode)
	}
	return resp, nil
}
