// Package oidc signs users in through an OpenID Connect provider and turns the // groups it reports into the access terdut grants. // // The package knows nothing about the database or HTTP handlers: Grants is a // pure function of configuration and groups, and Provider is the protocol. The // api package joins them to users, teams and sessions. package oidc import ( "git.ryuvia.com/niklas/terdut-server/internal/config" ) // Role names match models.RoleOwner and RoleMember. They are restated here so // the package stays free of the models import; config.Validate has already // refused anything else. const ( roleOwner = "owner" roleMember = "member" ) // Grants is the account-wide access a set of groups confers. Team access is a // separate question — see TeamGroup and ComputeTeamGrants — because it is // configured per team in the database, not in this package's cfg. type Grants struct { // Admitted is false when AllowedGroups is set and the user is in none of // them. Nothing else in the struct means anything then. Admitted bool // Admin is whether the user is in the admin group. Admin bool } // ComputeGrants evaluates the account-wide configuration against groups. func ComputeGrants(cfg config.OIDC, groups []string) Grants { in := make(map[string]bool, len(groups)) for _, g := range groups { in[g] = true } var g Grants g.Admitted = len(cfg.AllowedGroups) == 0 for _, allowed := range cfg.AllowedGroups { if in[allowed] { g.Admitted = true break } } if !g.Admitted { return g } g.Admin = cfg.AdminGroup != "" && in[cfg.AdminGroup] return g } // TeamGroup is one team's own OIDC binding: which group, if any, grants // member access to it and which grants owner access, as read from // teams.oidc_member_group / teams.oidc_owner_group. type TeamGroup struct { TeamID int64 MemberGroup string // "" means no group grants member access here. OwnerGroup string // "" means no group grants owner access here. } // ComputeTeamGrants evaluates every team's own group binding against groups, // and returns the role each team grants, keyed by team ID. A team absent from // the result is not granted at all. Where a team's member and owner groups // both match, the owner group wins — the same "highest role wins" rule that // applied across the old global mapping list applies here across one team's // two fields, so belonging to both groups makes somebody an owner rather than // whichever field happened to be checked last. func ComputeTeamGrants(teamGroups []TeamGroup, groups []string) map[int64]string { in := make(map[string]bool, len(groups)) for _, g := range groups { in[g] = true } out := map[int64]string{} for _, tg := range teamGroups { role := "" if tg.MemberGroup != "" && in[tg.MemberGroup] { role = roleMember } if tg.OwnerGroup != "" && in[tg.OwnerGroup] && rank(roleOwner) > rank(role) { role = roleOwner } if role != "" { out[tg.TeamID] = role } } return out } // rank orders roles; an unknown or absent role ranks lowest. func rank(role string) int { switch role { case roleOwner: return 2 case roleMember: return 1 } return 0 } // HigherRole reports whether role a outranks role b. func HigherRole(a, b string) bool { return rank(a) > rank(b) }