// Package shared defines cross-service primitives used by Game Lobby // application services: the Actor identity carried into every service // call, the authorization sentinel errors translated to the // `forbidden` HTTP code at the transport boundary, and the helpers // that derive one from a transport layer. package shared import ( "errors" "fmt" "strings" ) // ActorKind identifies the caller class of one Lobby service operation. type ActorKind string const ( // ActorKindAdmin reports that the caller is Admin Service routed through // the internal trusted HTTP port. Admin callers are pre-authorized by the // admin role check that Admin Service performs at the gateway boundary // before forwarding the request. ActorKindAdmin ActorKind = "admin" // ActorKindUser reports that the caller is an authenticated platform user // routed through Edge Gateway. User callers are identified by the // `X-User-ID` header injected by Edge Gateway. ActorKindUser ActorKind = "user" ) // IsKnown reports whether kind belongs to the frozen actor-kind vocabulary. func (kind ActorKind) IsKnown() bool { switch kind { case ActorKindAdmin, ActorKindUser: return true default: return false } } // Actor identifies the caller of one Lobby service operation. User actors // carry a non-empty UserID; admin actors carry an empty UserID. type Actor struct { // Kind reports the caller class. Kind ActorKind // UserID stores the platform user identifier for ActorKindUser callers. // It must be empty for ActorKindAdmin callers. UserID string } // NewAdminActor returns one Actor that identifies the trusted admin caller. func NewAdminActor() Actor { return Actor{Kind: ActorKindAdmin} } // NewUserActor returns one Actor that identifies the user caller with userID. func NewUserActor(userID string) Actor { return Actor{Kind: ActorKindUser, UserID: userID} } // IsAdmin reports whether actor is the trusted admin caller. func (actor Actor) IsAdmin() bool { return actor.Kind == ActorKindAdmin } // IsUser reports whether actor is an authenticated platform user. func (actor Actor) IsUser() bool { return actor.Kind == ActorKindUser } // Validate reports whether actor carries a structurally valid identity. // Admin actors must not carry a user identifier; user actors must carry a // non-empty trimmed user identifier. func (actor Actor) Validate() error { if !actor.Kind.IsKnown() { return fmt.Errorf("actor kind %q is unsupported", actor.Kind) } switch actor.Kind { case ActorKindAdmin: if strings.TrimSpace(actor.UserID) != "" { return fmt.Errorf("admin actor must not carry a user id") } case ActorKindUser: if strings.TrimSpace(actor.UserID) == "" { return fmt.Errorf("user actor must carry a non-empty user id") } if strings.TrimSpace(actor.UserID) != actor.UserID { return fmt.Errorf("user actor id must not contain surrounding whitespace") } } return nil } // ErrForbidden reports that the caller is not authorized for the requested // operation on the requested resource. The transport layer translates it to // the HTTP `403 forbidden` error envelope. var ErrForbidden = errors.New("forbidden") // ErrEligibilityDenied reports that the User Service eligibility snapshot // rejected the acting user for the requested operation. It covers both // "user not found" (Exists=false) and any sanction or marker that // collapses the relevant `can_*` flag to false. The transport layer // translates it to the HTTP `422 eligibility_denied` envelope. var ErrEligibilityDenied = errors.New("eligibility denied") // ErrServiceUnavailable reports that an upstream synchronous dependency // (User Service, Game Master, etc.) is unreachable or violated its // contract. The transport layer translates it to the HTTP // `503 service_unavailable` envelope. var ErrServiceUnavailable = errors.New("service unavailable") // ErrSubjectNotFound reports that the operation references a subject // (a user, a pending race-name registration, etc.) that is not present // in the relevant store and is not naturally surfaced through one of // the domain `ErrNotFound` sentinels. The transport layer translates // it to the HTTP `404 subject_not_found` envelope. var ErrSubjectNotFound = errors.New("subject not found")