// Package taskidentity derives workload identities from tasks assigned by Gitea. package taskidentity import ( "crypto/sha256" "errors" "fmt" "regexp" "strings" runnerv1 "gitea.dev/actionslib/runner/v1" ) var safeSegment = regexp.MustCompile(`^[A-Za-z0-9._-]+$`) // Identity is the trusted identity context extracted from a fetched task. type Identity struct { Repository string Task string SPIFFEID string } // FromTask derives the repository/task SPIFFE ID from Gitea's trusted task // context. Workflow input never supplies or overrides the resulting ID. func FromTask(task *runnerv1.Task, trustDomain string) (Identity, error) { if task == nil || task.Context == nil { return Identity{}, errors.New("task context is required") } repository := strings.TrimSpace(task.Context.GetFields()["repository"].GetStringValue()) taskName := strings.TrimSpace(task.Context.GetFields()["job"].GetStringValue()) parts := strings.Split(repository, "/") if len(parts) != 2 || parts[0] == "" || parts[1] == "" { return Identity{}, fmt.Errorf("invalid task repository %q", repository) } if taskName == "" { return Identity{}, errors.New("task job is required") } trustDomain = strings.TrimSpace(trustDomain) if trustDomain == "" || strings.ContainsAny(trustDomain, "/ ") { return Identity{}, fmt.Errorf("invalid trust domain %q", trustDomain) } path := strings.Join([]string{ sanitize(parts[0]), sanitize(parts[1]), sanitize(taskName), }, "/") return Identity{ Repository: repository, Task: taskName, SPIFFEID: "spiffe://" + trustDomain + "/ci/" + path, }, nil } // sanitize intentionally matches the bootstrap Python implementation so // existing Zot and OpenBao policies keep their current identity names. func sanitize(value string) string { if safeSegment.MatchString(value) { return value } slug := strings.Trim(regexp.MustCompile(`[^A-Za-z0-9._-]+`).ReplaceAllString(value, "-"), "-._") if len(slug) > 48 { slug = strings.TrimRight(slug[:48], "-._") } if slug == "" { slug = "segment" } digest := fmt.Sprintf("%x", sha256.Sum256([]byte(value)))[:12] return slug + "-" + digest }