6a3db171c1
Universal: PR Check / Branch Policy (pull_request) Successful in 1s
PR RC Release / Build RC Release (pull_request) Successful in 3s
Universal: PR Check / Validate PR (pull_request) Successful in 12s
Generic: Project CI / Lint & Validate (pull_request) Successful in 25s
Universal: PR Check / Secret Scan (pull_request) Successful in 1m2s
RC Revert / Rename rc/ back to dev/ (pull_request) Has been skipped
Branch Cleanup / Delete merged branch (pull_request) Successful in 4s
Generic: Project CI / Tests (pull_request) Has been cancelled
Universal: PR Check / Build RC Package (pull_request) Has been cancelled
Universal: PR Check / Report Issues (pull_request) Has been cancelled
Restricts which email domains an organization's members may have. When a policy
is configured, a user can only be added to the org (via any team) if their
primary email matches one of the allowed domain globs.
Enforced at the single membership choke point services/org.AddTeamMember, which
every add path (API, web, group-sync) funnels through — so one check covers them
all. On violation it returns a typed ErrEmailDomainNotAllowed; the API team-add
handler maps it to 422.
- models/git/org_email_domain.go: OrgEmailDomainPolicy model + EmailAllowed
(domain glob match) + OrgEmailDomainAllowed + typed error + CRUD. Migration 366.
- API: GET/PATCH/DELETE /orgs/{org}/email_domain_policy.
- Enforcement in services/org/team.go; 422 mapping in routers/api/v1/org/team.go.
An empty policy imposes no restriction. This is the one bounded piece of the
"access/security" tier; org 2FA-required and IP allowlists were deliberately NOT
built here — they are cross-cutting enforcement (auth gating / request
middleware) that needs a compiler + tests, not a blind stacked PR.
Stacked on #731/#730/#729/#728 for migration ordering (this = 366). Swagger
omitted.
Note: no Go toolchain available locally, so not compiled/gofmt'd/tested here.
Hand-verified: gofmt (tabs, no blank-in-block), imports (git_model added to the
api team handler, gci order), typed-error detection, migration contiguous (366).
Claude-Session: https://claude.ai/code/session_01Wsno14cxE49MstXFs9G5KT
135 lines
4.0 KiB
Go
135 lines
4.0 KiB
Go
// Copyright 2026 Moko Consulting <hello@mokoconsulting.tech>
|
|
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
|
|
package git
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/models/db"
|
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/glob"
|
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/log"
|
|
"code.mokoconsulting.tech/MokoConsulting/MokoGitea/modules/timeutil"
|
|
|
|
"xorm.io/builder"
|
|
)
|
|
|
|
// OrgEmailDomainPolicy restricts which email domains an organization's members may
|
|
// have. When configured, a user can only be added to the org if their primary email
|
|
// matches one of the allowed domain globs. At most one row per org. See issue #727.
|
|
type OrgEmailDomainPolicy struct {
|
|
ID int64 `xorm:"pk autoincr"`
|
|
OrgID int64 `xorm:"UNIQUE NOT NULL"`
|
|
AllowedDomains string `xorm:"TEXT"`
|
|
CreatedUnix timeutil.TimeStamp `xorm:"created"`
|
|
UpdatedUnix timeutil.TimeStamp `xorm:"updated"`
|
|
}
|
|
|
|
func init() {
|
|
db.RegisterModel(new(OrgEmailDomainPolicy))
|
|
}
|
|
|
|
// ErrEmailDomainNotAllowed is returned when a user's email domain is not permitted
|
|
// by the organization's email domain policy.
|
|
type ErrEmailDomainNotAllowed struct {
|
|
Email string
|
|
OrgID int64
|
|
}
|
|
|
|
func (e ErrEmailDomainNotAllowed) Error() string {
|
|
return fmt.Sprintf("email %q is not in an allowed domain for organization %d", e.Email, e.OrgID)
|
|
}
|
|
|
|
// IsErrEmailDomainNotAllowed reports whether err is an ErrEmailDomainNotAllowed.
|
|
func IsErrEmailDomainNotAllowed(err error) bool {
|
|
_, ok := err.(ErrEmailDomainNotAllowed)
|
|
return ok
|
|
}
|
|
|
|
func (p *OrgEmailDomainPolicy) domainGlobs() []glob.Glob {
|
|
var out []glob.Glob
|
|
for _, d := range strings.Split(p.AllowedDomains, ";") {
|
|
d = strings.TrimSpace(strings.ToLower(d))
|
|
if d == "" {
|
|
continue
|
|
}
|
|
if g, err := glob.Compile(d); err == nil {
|
|
out = append(out, g)
|
|
} else {
|
|
log.Warn("Invalid org email domain glob %q: %v", d, err)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// EmailAllowed reports whether email's domain satisfies the policy. An empty policy
|
|
// (no configured domains) allows any email.
|
|
func (p *OrgEmailDomainPolicy) EmailAllowed(email string) bool {
|
|
globs := p.domainGlobs()
|
|
if len(globs) == 0 {
|
|
return true
|
|
}
|
|
at := strings.LastIndexByte(email, '@')
|
|
if at < 0 {
|
|
return false
|
|
}
|
|
domain := strings.ToLower(email[at+1:])
|
|
for _, g := range globs {
|
|
if g.Match(domain) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// GetOrgEmailDomainPolicy returns the org's email domain policy, or nil if none.
|
|
func GetOrgEmailDomainPolicy(ctx context.Context, orgID int64) (*OrgEmailDomainPolicy, error) {
|
|
policy, exist, err := db.Get[OrgEmailDomainPolicy](ctx, builder.Eq{"org_id": orgID})
|
|
if err != nil {
|
|
return nil, err
|
|
} else if !exist {
|
|
return nil, nil //nolint:nilnil
|
|
}
|
|
return policy, nil
|
|
}
|
|
|
|
// OrgEmailDomainAllowed reports whether email is permitted for the org. It returns
|
|
// true when the org has no policy configured.
|
|
func OrgEmailDomainAllowed(ctx context.Context, orgID int64, email string) (bool, error) {
|
|
policy, err := GetOrgEmailDomainPolicy(ctx, orgID)
|
|
if err != nil {
|
|
return false, err
|
|
}
|
|
if policy == nil {
|
|
return true, nil
|
|
}
|
|
return policy.EmailAllowed(email), nil
|
|
}
|
|
|
|
// UpsertOrgEmailDomainPolicy creates or updates the single policy row for an org.
|
|
func UpsertOrgEmailDomainPolicy(ctx context.Context, policy *OrgEmailDomainPolicy) error {
|
|
existing, err := GetOrgEmailDomainPolicy(ctx, policy.OrgID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if existing == nil {
|
|
if _, err := db.GetEngine(ctx).Insert(policy); err != nil {
|
|
return fmt.Errorf("Insert OrgEmailDomainPolicy: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
policy.ID = existing.ID
|
|
if _, err := db.GetEngine(ctx).ID(existing.ID).AllCols().Update(policy); err != nil {
|
|
return fmt.Errorf("Update OrgEmailDomainPolicy: %v", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// DeleteOrgEmailDomainPolicy removes an org's email domain policy.
|
|
func DeleteOrgEmailDomainPolicy(ctx context.Context, orgID int64) error {
|
|
_, err := db.GetEngine(ctx).Where("org_id = ?", orgID).Delete(new(OrgEmailDomainPolicy))
|
|
return err
|
|
}
|