~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
173 lines · 6.8 KB Raw History
package compose

import (
	"net"
	"net/url"
	"slices"
	"strings"

	"github.com/johnandersen777/socialweb-computer-kcp/api/v1alpha1"
	infrarecord "github.com/johnandersen777/socialweb-computer-kcp/internal/atproto/infra"
	atprotooauth "github.com/johnandersen777/socialweb-computer-kcp/internal/atproto/oauth"
	"github.com/johnandersen777/socialweb-computer-kcp/internal/openbaoctl"
)

const (
	AtprotoPluginName         = "atproto"
	AtprotoMountPath          = "atproto"
	AtprotoUserRole           = "user"
	AtprotoSelfPolicy         = "atproto-self"
	AtprotoSessionProxyPolicy = "atproto-session-proxy"
	AtprotoClientMetadataPath = "/oauth-client-metadata.json"
	AtprotoCallbackPath       = "/oauth/callback"
	AtprotoUserTokenTTL       = "1h"
	AtprotoUserTokenMaxTTL    = "720h"
	atprotoClientName         = "socialweb-computer"
	PublicOriginOption        = "public-origin"
)

var requesterScope = []string{
	"atproto",
	"repo:com.publicdomainrelay.temp.compute.vm?action=create&action=update",
	"repo:com.publicdomainrelay.temp.market.rfp?action=create&action=update",
	"repo:com.publicdomainrelay.temp.market.accept?action=create&action=update",
	"repo:com.publicdomainrelay.temp.market.event?action=create&action=update",
	"repo:com.publicdomainrelay.temp.compute.events.vm.delete?action=create&action=update",
	"repo:com.publicdomainrelay.temp.compute.events.vm.onNetwork?action=create&action=update",
	"repo:com.publicdomainrelay.temp.compute.events.guest.publicKey?action=create&action=update",
	"repo:com.publicdomainrelay.temp.compute.events.guest.signalWebRTC?action=create&action=update",
	"repo:com.publicdomainrelay.temp.compute.events.guest.secrets?action=create&action=update",
	"repo:com.fedproxy.rbac?action=create&action=update&action=delete",
	"repo:computer.socialweb.temp.policy.ghalite?action=create&action=update",
	"repo:computer.socialweb.temp.policy.typescript?action=create&action=update",
	"repo:com.publicdomainrelay.temp.badgeBlueKeys?action=create&action=update&action=delete",
	"repo:com.publicdomainrelay.temp.market.heartbeat?action=create&action=update",
	"repo:com.publicdomainrelay.temp.market.association?action=create&action=update&action=delete",
	"repo:" + infrarecord.InfraNSID + "?action=create&action=update&action=delete",
	"repo:" + infrarecord.EphemeralInfraNSID + "?action=create&action=update&action=delete",
	"rpc:com.publicdomainrelay.temp.market.submitRfp?aud=*",
	"rpc:com.publicdomainrelay.temp.market.submitAccept?aud=*",
	"rpc:com.publicdomainrelay.temp.market.submitBid?aud=*",
	"rpc:com.publicdomainrelay.temp.market.submitEvent?aud=*",
}

var clusterScope = []string{
	"rpc:com.publicdomainrelay.temp.cluster.createCluster?aud=*",
	"rpc:com.publicdomainrelay.temp.cluster.listClusters?aud=*",
	"rpc:com.publicdomainrelay.temp.cluster.getKubeconfig?aud=*",
	"rpc:com.publicdomainrelay.temp.cluster.controlPlaneUp?aud=*",
	"rpc:com.publicdomainrelay.temp.cluster.controlPlaneDown?aud=*",
	"rpc:com.publicdomainrelay.temp.cluster.deleteCluster?aud=*",
	"repo:" + infrarecord.InfraNSID + "?action=create&action=update&action=delete",
}

func GatewayAtprotoScope() string {
	var out []string
	for _, scope := range append(slices.Clone(requesterScope), clusterScope...) {
		if !slices.Contains(out, scope) {
			out = append(out, scope)
		}
	}
	return strings.Join(out, " ")
}

var sshAtircScope = []string{atprotooauth.RPCAtircMint, atprotooauth.RPCAtircRevoke}

func SshGatewayAtprotoScope() string {
	return strings.Join(slices.Concat(requesterScope, sshAtircScope), " ")
}

func OptionValue(opts []v1alpha1.ApplicationOption, name string) string {
	for _, o := range opts {
		if o.Name == name {
			return strings.TrimRight(strings.TrimSpace(o.Value), "/")
		}
	}
	return ""
}

func AtprotoPolicies() []v1alpha1.OpenBaoPolicy {
	own := "auth/" + AtprotoMountPath + "/session/{{identity.entity.aliases." + openbaoctl.AccessorPlaceholder(AtprotoMountPath) + ".name}}"
	anyDID := "auth/" + AtprotoMountPath + "/session/+"
	cu := []string{"create", "update"}
	return []v1alpha1.OpenBaoPolicy{
		{Name: AtprotoSelfPolicy, Rules: []v1alpha1.OpenBaoPolicyRule{
			{Path: own, Capabilities: []string{"read", "delete"}},
			{Path: own + "/proof", Capabilities: cu},
		}},
		{Name: AtprotoSessionProxyPolicy, Rules: []v1alpha1.OpenBaoPolicyRule{
			{Path: anyDID, Capabilities: []string{"read"}},
			{Path: anyDID + "/proof", Capabilities: cu},
		}},
	}
}

func atprotoTrust(n *v1alpha1.MarketNetworkValue) (plc string, hosts []string, caPEM string, err error) {
	if n == nil {
		return "", nil, "", nil
	}
	for _, h := range n.Hosts {
		switch {
		case strings.HasPrefix(h.Name, "plc."):
			plc = plcScheme(n) + "://" + h.Name
			hosts = append(hosts, h.Name+"="+h.Address)
		case strings.HasPrefix(h.Name, "pds."):
			hosts = append(hosts, h.Name+"="+h.Address)
		}
	}
	if u, uerr := url.Parse(n.GuestTLSURL()); n.GuestTLSURL() != "" && uerr == nil && u.Port() != "" {
		hosts = append(hosts, "*."+u.Hostname()+"=127.0.0.1:"+u.Port())
	}
	if plc == "" && n.PLCDirectoryURL != "" {
		plc = n.PLCDirectoryURL
		if rest, ok := strings.CutPrefix(plc, "https://"); ok {
			hosts = append(hosts, strings.TrimRight(rest, "/"))
		}
	}
	if len(hosts) > 0 {
		caPEM = strings.TrimSpace(n.CABundle)
	}
	return plc, hosts, caPEM, nil
}

func AtprotoPlugin(opts []v1alpha1.ApplicationOption, n *v1alpha1.MarketNetworkValue, scope string) (v1alpha1.OpenBaoPlugin, error) {
	plc, hosts, caPEM, err := atprotoTrust(n)
	if err != nil {
		return v1alpha1.OpenBaoPlugin{}, err
	}
	a := &v1alpha1.OpenBaoAtprotoPlugin{Roles: []v1alpha1.OpenBaoAtprotoRole{{Name: AtprotoUserRole, BoundDIDs: []string{"*"},
		Policies: []string{AtprotoSelfPolicy}, TokenTTL: AtprotoUserTokenTTL, TokenMaxTTL: AtprotoUserTokenMaxTTL}}}
	if origin := OptionValue(opts, PublicOriginOption); origin != "" {
		if trusted := loopbackOriginHost(origin); trusted != "" && !slices.Contains(hosts, trusted) {
			hosts = append(hosts, trusted)
		}
		a.Config = v1alpha1.OpenBaoAtprotoConfig{ClientID: origin + AtprotoClientMetadataPath, RedirectURIs: []string{origin + AtprotoCallbackPath},
			ClientName: atprotoClientName, Scope: scope, PLCDirectoryURL: plc, TrustedHosts: hosts, TrustedCAPEM: caPEM}
	}
	return v1alpha1.OpenBaoPlugin{Name: AtprotoPluginName, Type: "auth", Asset: v1alpha1.HostProcessAsset{Name: openbaoctl.AtprotoPluginAsset},
		MountPath: AtprotoMountPath, Atproto: a}, nil
}

func loopbackOriginHost(origin string) string {
	parsed, err := url.Parse(origin)
	if err != nil || parsed.Host == "" || !strings.EqualFold(parsed.Scheme, "http") {
		return ""
	}
	host := parsed.Hostname()
	if !strings.EqualFold(host, "localhost") {
		ip := net.ParseIP(host)
		if ip == nil || !ip.IsLoopback() {
			return ""
		}
	}
	return parsed.Host
}

func plcScheme(n *v1alpha1.MarketNetworkValue) string {
	if n != nil {
		if parsed, err := url.Parse(n.PLCDirectoryURL); err == nil && strings.EqualFold(parsed.Scheme, "http") {
			return "http"
		}
	}
	return "https"
}