~ knotless Search/
↑↓ move↵ open⇧↵ all results
0
0
main
301 lines · 11.6 KB Raw History
package oauth

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

const (
	CollectionRBACDID               = "com.publicdomainrelay.temp.auth.allowlist.rbacDid"
	CollectionOffering              = "com.publicdomainrelay.temp.market.offering"
	CollectionBid                   = "com.publicdomainrelay.temp.market.bid"
	CollectionBidsFree              = "com.publicdomainrelay.temp.market.bids.free"
	CollectionBidsX402              = "com.publicdomainrelay.temp.market.bids.x402"
	CollectionReceipt               = "com.publicdomainrelay.temp.market.receipt"
	CollectionReceiptsFree          = "com.publicdomainrelay.temp.market.receipts.free"
	CollectionReceiptsX402          = "com.publicdomainrelay.temp.market.receipts.x402"
	CollectionEvent                 = "com.publicdomainrelay.temp.market.event"
	CollectionConfigWIFSimple       = "com.publicdomainrelay.temp.compute.config.wif.simple"
	CollectionComputeVM             = "com.publicdomainrelay.temp.compute.vm"
	CollectionRFP                   = "com.publicdomainrelay.temp.market.rfp"
	CollectionAccept                = "com.publicdomainrelay.temp.market.accept"
	CollectionVMDelete              = "com.publicdomainrelay.temp.compute.events.vm.delete"
	CollectionVMOnNetwork           = "com.publicdomainrelay.temp.compute.events.vm.onNetwork"
	CollectionVMTerminated          = "com.publicdomainrelay.temp.compute.events.vm.terminated"
	CollectionVMHostKeys            = "com.publicdomainrelay.temp.compute.events.vm.hostKeys"
	CollectionGuestPublicKey        = "com.publicdomainrelay.temp.compute.events.guest.publicKey"
	CollectionGuestSignalWebRTC     = "com.publicdomainrelay.temp.compute.events.guest.signalWebRTC"
	CollectionGuestSecrets          = "com.publicdomainrelay.temp.compute.events.guest.secrets"
	CollectionGuestSecretsApplied   = "com.publicdomainrelay.temp.compute.events.guest.secretsApplied"
	CollectionGuestSecretsRequested = "com.publicdomainrelay.temp.compute.events.guest.secretsRequested"
	CollectionGuestAgentStart       = "com.publicdomainrelay.temp.compute.events.guest.agentStart"
	CollectionGuestRequesterDID     = "com.publicdomainrelay.temp.compute.events.guest.requesterDid"
	CollectionGuestSecretsUpdated   = "com.publicdomainrelay.temp.compute.events.guest.secretsUpdated"
	CollectionGuestR2Start          = "com.publicdomainrelay.temp.compute.events.guest.r2Start"
	CollectionGuestR2Release        = "com.publicdomainrelay.temp.compute.events.guest.r2Release"
	CollectionGuestR2Released       = "com.publicdomainrelay.temp.compute.events.guest.r2Released"
	CollectionBadgeBlueKeys         = "com.publicdomainrelay.temp.badgeBlueKeys"
	CollectionHeartbeat             = "com.publicdomainrelay.temp.market.heartbeat"
	CollectionFedproxyRBAC          = "com.fedproxy.rbac"
	CollectionFedproxySSHKey        = "com.fedproxy.sshPublicKey"
	CollectionBidderAssociation     = "com.publicdomainrelay.temp.market.bidderAssociation"
	CollectionAssociation           = "com.publicdomainrelay.temp.market.association"
	CollectionAssociationRequest    = "com.publicdomainrelay.temp.market.associationRequest"
	CollectionPolicyGhaLite         = "computer.socialweb.temp.policy.ghalite"
	CollectionPolicyTypeScript      = "computer.socialweb.temp.policy.typescript"
	CollectionInfra                 = "computer.socialweb.infra"
	CollectionEphemeralInfra        = "computer.socialweb.ephemeralInfra"
	CollectionIngressWebRTC         = "com.publicdomainrelay.temp.ingress.webRTC"
	CollectionTangledVouch          = "sh.tangled.graph.vouch"

	RepoIngressWebRTCCreate = "repo:" + CollectionIngressWebRTC + "?action=create"
	RepoTangledVouchWrite   = "repo:" + CollectionTangledVouch + "?action=create&action=update"

	RPCSubmitRFP         = "rpc:com.publicdomainrelay.temp.market.submitRfp?aud=*"
	RPCSubmitAccept      = "rpc:com.publicdomainrelay.temp.market.submitAccept?aud=*"
	RPCSubmitBid         = "rpc:com.publicdomainrelay.temp.market.submitBid?aud=*"
	RPCSubmitEvent       = "rpc:com.publicdomainrelay.temp.market.submitEvent?aud=*"
	RPCAssociateConfirm  = "rpc:com.publicdomainrelay.temp.requester.associateConfirm?aud=*"
	RPCClusterCreate     = "rpc:com.publicdomainrelay.temp.cluster.createCluster?aud=*"
	RPCClusterList       = "rpc:com.publicdomainrelay.temp.cluster.listClusters?aud=*"
	RPCClusterKubeconfig = "rpc:com.publicdomainrelay.temp.cluster.getKubeconfig?aud=*"
	RPCClusterUp         = "rpc:com.publicdomainrelay.temp.cluster.controlPlaneUp?aud=*"
	RPCClusterDown       = "rpc:com.publicdomainrelay.temp.cluster.controlPlaneDown?aud=*"
	RPCClusterDelete     = "rpc:com.publicdomainrelay.temp.cluster.deleteCluster?aud=*"
	RPCAtircMint         = "rpc:chat.atirc.mint?aud=did:web:irc.fedfork.com%23atirc"
	RPCAtircRevoke       = "rpc:chat.atirc.revoke?aud=did:web:irc.fedfork.com%23atirc"
)

var (
	RequesterOAuthScope = dedupeScopes(
		[]string{ScopeAtproto},
		collectionScopes([]string{"create", "update"},
			CollectionComputeVM, CollectionRFP, CollectionAccept, CollectionEvent, CollectionVMDelete,
			CollectionVMOnNetwork, CollectionGuestPublicKey, CollectionGuestSignalWebRTC, CollectionGuestSecrets,
			CollectionGuestAgentStart, CollectionGuestSecretsUpdated, CollectionGuestR2Start, CollectionGuestR2Release,
			CollectionPolicyGhaLite, CollectionPolicyTypeScript),
		[]string{collectionScope(CollectionFedproxyRBAC, "create", "update", "delete")},
		[]string{collectionScope(CollectionBadgeBlueKeys, "create", "update", "delete")},
		[]string{collectionScope(CollectionAssociation, "create", "update", "delete")},
		[]string{collectionScope(CollectionAssociationRequest, "create", "update", "delete")},
		collectionScopes([]string{"create", "update", "delete"}, CollectionInfra, CollectionEphemeralInfra),
		[]string{RPCSubmitRFP, RPCSubmitAccept, RPCSubmitBid, RPCSubmitEvent},
		[]string{RPCAtircMint, RPCAtircRevoke},
		[]string{RepoTangledVouchWrite},
	)

	BidderOAuthScope = dedupeScopes(
		[]string{ScopeAtproto},
		collectionScopes([]string{"create"},
			CollectionRBACDID, CollectionBid, CollectionBidsFree, CollectionReceipt, CollectionEvent,
			CollectionConfigWIFSimple, CollectionComputeVM, CollectionRFP, CollectionAccept,
			CollectionVMDelete, CollectionVMOnNetwork, CollectionVMTerminated, CollectionVMHostKeys,
			CollectionGuestPublicKey, CollectionGuestSignalWebRTC, CollectionGuestSecretsApplied,
			CollectionGuestSecretsRequested, CollectionGuestRequesterDID, CollectionGuestR2Released,
			CollectionFedproxyRBAC, CollectionBidderAssociation, CollectionPolicyGhaLite,
			CollectionPolicyTypeScript),
		[]string{collectionScope(CollectionOffering, "create", "update")},
		[]string{collectionScope(CollectionHeartbeat, "create", "update")},
		[]string{collectionScope(CollectionBadgeBlueKeys, "create", "update", "delete")},
		[]string{collectionScope(CollectionAssociation, "create", "update", "delete")},
		[]string{RPCSubmitRFP, RPCSubmitAccept, RPCSubmitBid, RPCSubmitEvent},
	)

	DesktopOAuthScope = dedupeScopes(
		[]string{ScopeAtproto},
		collectionScopes([]string{"create", "update"},
			CollectionRBACDID, CollectionOffering, CollectionBid, CollectionBidsFree, CollectionBidsX402,
			CollectionReceipt, CollectionReceiptsFree, CollectionReceiptsX402, CollectionEvent,
			CollectionConfigWIFSimple, CollectionFedproxyRBAC, CollectionBidderAssociation),
		[]string{collectionScope(CollectionBadgeBlueKeys, "create")},
		[]string{RPCSubmitBid, RPCSubmitEvent},
	)

	ComputeSPAOAuthScope = dedupeScopes(
		[]string{ScopeAtproto},
		collectionScopes([]string{"create"},
			CollectionComputeVM, CollectionBadgeBlueKeys, CollectionFedproxyRBAC, CollectionFedproxySSHKey),
		[]string{RPCSubmitRFP, RPCSubmitAccept},
	)

	DIDKeyAssociatorOAuthScope = dedupeScopes(
		BidderOAuthScope,
		RequesterOAuthScope,
		[]string{RPCAssociateConfirm},
	)

	K8sOAuthScope = dedupeScopes(
		RequesterOAuthScope,
		[]string{RPCClusterCreate, RPCClusterList, RPCClusterKubeconfig, RPCClusterUp, RPCClusterDown, RPCClusterDelete},
	)

	OAuthScope = dedupeScopes(
		[]string{ScopeAtproto},
		RequesterOAuthScope,
		BidderOAuthScope,
		DesktopOAuthScope,
		ComputeSPAOAuthScope,
		DIDKeyAssociatorOAuthScope,
		K8sOAuthScope,
	)
)

var OptionalScopes = []string{RPCAtircRevoke, RepoTangledVouchWrite}

func RequiredScopes(requested []string) []string {
	out := make([]string, 0, len(requested))
	for _, scope := range requested {
		if !slices.Contains(OptionalScopes, scope) {
			out = append(out, scope)
		}
	}
	return out
}

func ScopeString(scopes []string) string {
	return strings.Join(scopes, " ")
}

const ScopeTransitionGeneric = "transition:generic"

var repoActions = []string{"create", "update", "delete"}

func MissingScopes(required, granted []string) []string {
	have := map[string]bool{}
	for _, scope := range granted {
		have[scope] = true
	}
	missing := []string{}
	for _, want := range required {
		if have[want] || grantedBy(want, granted, have) {
			continue
		}
		missing = append(missing, want)
	}
	return missing
}

var transitionScopes = []string{ScopeTransitionGeneric, "transition:email", "transition:chat.bsky"}

var permissionResources = []string{"repo", "rpc", "blob", "identity", "account", "include"}

func ValidScope(scope string) bool {
	if scope == "" {
		return false
	}
	for _, c := range scope {
		if c < 0x21 || c > 0x7e {
			return false
		}
	}
	if scope == ScopeAtproto || slices.Contains(transitionScopes, scope) {
		return true
	}
	head, query, _ := strings.Cut(scope, "?")
	resource, _, _ := strings.Cut(head, ":")
	if !slices.Contains(permissionResources, resource) {
		return false
	}
	_, err := url.ParseQuery(query)
	return err == nil
}

func KnownGrant(granted []string) bool {
	return len(granted) > 0 && !slices.ContainsFunc(granted, func(scope string) bool { return !ValidScope(scope) })
}

func ScopeSubject(scope string) string {
	resource, subject, _ := parsePermission(scope)
	if subject == "" || (resource != "repo" && resource != "rpc") {
		return scope
	}
	return subject
}

func grantedBy(want string, granted []string, have map[string]bool) bool {
	resource, subject, params := parsePermission(want)
	if resource != "repo" && resource != "rpc" {
		return false
	}
	if have[ScopeTransitionGeneric] {
		return true
	}
	needed := map[string]bool{}
	if resource == "repo" {
		for _, action := range orAll(params["action"]) {
			needed[action] = true
		}
	}
	for _, scope := range granted {
		gotResource, gotSubject, got := parsePermission(scope)
		if gotResource != resource || (gotSubject != subject && gotSubject != "*") {
			continue
		}
		if resource == "rpc" {
			if aud := got.Get("aud"); aud == "*" || aud == params.Get("aud") {
				return true
			}
			continue
		}
		for _, action := range orAll(got["action"]) {
			delete(needed, action)
		}
		if len(needed) == 0 {
			return true
		}
	}
	return false
}

func parsePermission(scope string) (string, string, url.Values) {
	head, query, _ := strings.Cut(strings.TrimSpace(scope), "?")
	resource, subject, _ := strings.Cut(head, ":")
	params, err := url.ParseQuery(query)
	if err != nil {
		params = url.Values{}
	}
	if subject == "" {
		switch resource {
		case "repo":
			subject = params.Get("collection")
		case "rpc":
			subject = params.Get("lxm")
		}
	}
	return resource, subject, params
}

func orAll(actions []string) []string {
	if len(actions) == 0 {
		return repoActions
	}
	return actions
}

func collectionScope(nsid string, verbs ...string) string {
	return "repo:" + nsid + "?action=" + strings.Join(verbs, "&action=")
}

func collectionScopes(verbs []string, nsids ...string) []string {
	out := make([]string, 0, len(nsids))
	for _, nsid := range nsids {
		out = append(out, collectionScope(nsid, verbs...))
	}
	return out
}

func dedupeScopes(sets ...[]string) []string {
	seen := map[string]bool{}
	out := []string{}
	for _, set := range sets {
		for _, token := range set {
			if seen[token] {
				continue
			}
			seen[token] = true
			out = append(out, token)
		}
	}
	return out
}