~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
112 lines · 3.9 KB Raw History
package tenantcredentialmain

import (
	"fmt"
	"strings"
	"time"
)

const (
	envStateDir = "SOCIALWEB_TENANTCREDENTIAL_STATE_DIR"

	envCluster = "SOCIALWEB_TENANTCREDENTIAL_CLUSTER"

	envServer = "SOCIALWEB_TENANTCREDENTIAL_SERVER"

	envCommonName = "SOCIALWEB_TENANTCREDENTIAL_COMMON_NAME"

	envOrganizations = "SOCIALWEB_TENANTCREDENTIAL_ORGANIZATIONS"

	envOutDir = "SOCIALWEB_TENANTCREDENTIAL_OUT_DIR"

	envValidFor = "SOCIALWEB_TENANTCREDENTIAL_VALID_FOR"
)

type Config struct {
	StateDir string

	Cluster string

	Server string

	CommonName string

	Organizations []string

	OutDir string

	ValidFor time.Duration
}

func load(get func(string) string) (Config, error) {
	cfg := Config{
		StateDir:   strings.TrimSpace(get(envStateDir)),
		Cluster:    strings.TrimSpace(get(envCluster)),
		Server:     strings.TrimSpace(get(envServer)),
		CommonName: strings.TrimSpace(get(envCommonName)),
		OutDir:     strings.TrimSpace(get(envOutDir)),
	}

	for _, req := range []struct{ name, value, why string }{
		{envStateDir, cfg.StateDir,
			"it is the clusterprovider's state dir, where each cluster's data dir lives, and the " +
				"cluster's own ClientCA -- the only thing a tenant credential can be signed with -- " +
				"is under it. With no state dir there is no CA to sign with, and a run that guessed " +
				"one would sign against whatever CA happened to sit in the working directory"},
		{envCluster, cfg.Cluster,
			"it names which data dir under the state dir to sign from, because a host can hold " +
				"several clusters and each has its own ClientCA. An unset cluster is a credential " +
				"minted from no cluster, or from whichever one a default picked, which is a " +
				"credential for a cluster the operator did not name"},
		{envServer, cfg.Server,
			"it is the https URL of the tenant apiserver this credential authenticates to, and it " +
				"is also the host name the apiserver's own certificate must carry. With no URL there " +
				"is no server identity to check, and the credential would verify nothing"},
		{envCommonName, cfg.CommonName,
			"it is the username the apiserver reads off the certificate. An unset one cannot be " +
				"compared against what was minted, and that comparison is the only thing that catches " +
				"a credential whose identity is not the one the operator's RBAC binds"},
		{envOutDir, cfg.OutDir,
			"it is where ca.crt, tls.crt and tls.key are written, and there is no default for it: a " +
				"credential written to a guessed directory is one the operator cannot find and cannot " +
				"delete"},
	} {
		if req.value == "" {
			return Config{}, fmt.Errorf("%s is not set. %s", req.name, req.why)
		}
	}

	raw := strings.TrimSpace(get(envOrganizations))
	if raw == "" {
		return Config{}, fmt.Errorf(
			"%s is not set. The apiserver reads the groups off the certificate's Organizations, so "+
				"a credential with no organization carries no group and authenticates as an "+
				"unprivileged user: every object the tenant grant exists to apply would be refused, "+
				"and the refusal reads as a broken grant rather than as a credential that was asked "+
				"for without one", envOrganizations)
	}
	parts := strings.Split(raw, ",")
	organizations := make([]string, 0, len(parts))
	for _, part := range parts {
		organizations = append(organizations, strings.TrimSpace(part))
	}
	cfg.Organizations = organizations

	rawValid := strings.TrimSpace(get(envValidFor))
	if rawValid != "" {
		d, err := time.ParseDuration(rawValid)
		if err != nil {
			return Config{}, fmt.Errorf("%s is %q and it is not a Go duration such as 720h: %w",
				envValidFor, rawValid, err)
		}
		if d <= 0 {
			return Config{}, fmt.Errorf(
				"%s is %q. A non-positive validity is not honoured by the minting path, which "+
					"substitutes its own default, so an operator who asked for it would believe they "+
					"had it: the duration is refused rather than silently replaced",
				envValidFor, rawValid)
		}
		cfg.ValidFor = d
	}
	return cfg, nil
}