// Package access holds the rules that decide who may see or share a // repository. It deliberately imports nothing from core: the visibility // rule is used by the 404 gate, the index module, share-link minting, and // share-link redemption, and putting it in one dependency-free place is // what keeps those four from drifting apart. package access import ( "strings" "time" ) // Policy is the authorization-relevant subset of a repository's config. // core.RepoInfo is the full config; Policy is what the rules below need. type Policy struct { Slug string Hidden bool VisibleTo []string ShareRevokedAfter time.Time } // Identity is the verified caller. SharedSlugs/SharedIssuedAt come from a // valid gsg_share cookie and are empty when the caller has no share grant. type Identity struct { User string SharedSlugs []string SharedIssuedAt time.Time } // CanView reports whether id may see the repository at all. Public repos // are visible to everyone; a hidden repo is visible to an owner named in // gsg.visible-to or to an unrevoked share grant. func (p Policy) CanView(id Identity) bool { if !p.Hidden { return true } return p.canViewAsOwner(id.User) || (p.coversShare(id.SharedSlugs) && ShareLive(p.ShareRevokedAfter, id.SharedIssuedAt)) } // CanShare reports whether user may mint a share link for this repository. // Only hidden repos are shareable (sharing a public repo would grant // nothing new), and only an owner may mint -- a share grant cannot be // amplified into a broader grant. func (p Policy) CanShare(user string) bool { return p.Hidden && p.canViewAsOwner(user) } // canViewAsOwner matches gsg.visible-to case-insensitively. Anonymous // callers never match: "" would otherwise vacuous-match a blank entry. func (p Policy) canViewAsOwner(user string) bool { if user == "" { return false } for _, u := range p.VisibleTo { if strings.EqualFold(u, user) { return true } } return false } func (p Policy) coversShare(slugs []string) bool { for _, s := range slugs { if s == p.Slug { return true } } return false } // ShareLive reports whether a grant issued at issuedAt survives a // repository's gsg.share-revoked-after value. A zero revokedAfter means no // revocation; a grant issued at or before the timestamp is revoked, which // is why the comparison is strict -- the operator's intent is "everything // issued before this moment is dead", and the two sides are second-granular. // // The free function is for callers (share redemption) that only have the // timestamp, not a full Policy. func ShareLive(revokedAfter, issuedAt time.Time) bool { return revokedAfter.IsZero() || issuedAt.After(revokedAfter) }