git.iko.soy/typeable/oauth2-proxy
on master help sign in
typeable/oauth2-proxy/validator.go
133 lines 3.7 kB history raw
1package main
3import (
4 "encoding/csv"
5 "io"
6 "os"
7 "strings"
8 "sync/atomic"
9 "unsafe"
11 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/logger"
12 "github.com/oauth2-proxy/oauth2-proxy/v7/pkg/watcher"
15// UserMap holds information from the authenticated emails file
16type UserMap struct {
17 usersFile string
18 m unsafe.Pointer
21// NewUserMap parses the authenticated emails file into a new UserMap
22//
23// TODO (@NickMeves): Audit usage of `unsafe.Pointer` and potentially refactor
24func NewUserMap(usersFile string, done <-chan bool, onUpdate func()) *UserMap {
25 um := &UserMap{usersFile: usersFile}
26 m := make(map[string]bool)
27 atomic.StorePointer(&um.m, unsafe.Pointer(&m)) // #nosec G103
28 if usersFile != "" {
29 logger.Printf("using authenticated emails file %s", usersFile)
30 watcher.WatchFileForUpdates(usersFile, done, func() {
31 um.LoadAuthenticatedEmailsFile()
32 onUpdate()
33 })
34 um.LoadAuthenticatedEmailsFile()
35 }
36 return um
39// IsValid checks if an email is allowed
40func (um *UserMap) IsValid(email string) (result bool) {
41 m := *(*map[string]bool)(atomic.LoadPointer(&um.m))
42 _, result = m[email]
43 return
46// LoadAuthenticatedEmailsFile loads the authenticated emails file from disk
47// and parses the contents as CSV
48func (um *UserMap) LoadAuthenticatedEmailsFile() {
49 r, err := os.Open(um.usersFile)
50 if err != nil {
51 logger.Fatalf("failed opening authenticated-emails-file=%q, %s", um.usersFile, err)
52 }
53 defer func(c io.Closer) {
54 cerr := c.Close()
55 if cerr != nil {
56 logger.Fatalf("Error closing authenticated emails file: %s", cerr)
57 }
58 }(r)
59 csvReader := csv.NewReader(r)
60 csvReader.Comma = ','
61 csvReader.Comment = '#'
62 csvReader.TrimLeadingSpace = true
63 records, err := csvReader.ReadAll()
64 if err != nil {
65 logger.Errorf("error reading authenticated-emails-file=%q, %s", um.usersFile, err)
66 return
67 }
68 updated := make(map[string]bool)
69 for _, r := range records {
70 address := strings.ToLower(strings.TrimSpace(r[0]))
71 updated[address] = true
72 }
73 atomic.StorePointer(&um.m, unsafe.Pointer(&updated)) // #nosec G103
76func newValidatorImpl(domains []string, usersFile string,
77 done <-chan bool, onUpdate func()) func(string) bool {
78 validUsers := NewUserMap(usersFile, done, onUpdate)
80 var allowAll bool
81 for i, domain := range domains {
82 if domain == "*" {
83 allowAll = true
84 continue
85 }
86 domains[i] = strings.ToLower(domain)
87 }
89 validator := func(email string) (valid bool) {
90 if email == "" {
91 return
92 }
93 email = strings.ToLower(email)
94 valid = isEmailValidWithDomains(email, domains)
95 if !valid {
96 valid = validUsers.IsValid(email)
97 }
98 if allowAll {
99 valid = true
100 }
101 return valid
102 }
103 return validator
106// NewValidator constructs a function to validate email addresses
107func NewValidator(domains []string, usersFile string) func(string) bool {
108 return newValidatorImpl(domains, usersFile, nil, func() {})
111// isEmailValidWithDomains checks if the authenticated email is validated against the provided domain
112func isEmailValidWithDomains(email string, allowedDomains []string) bool {
113 if strings.Count(email, "@") != 1 {
114 return false
115 }
117 for _, domain := range allowedDomains {
118 // allow if the domain is perfect suffix match with the email
119 if strings.HasSuffix(email, "@"+domain) {
120 return true
121 }
123 // allow if the domain is prefixed with . or *. and
124 // the last element (split on @) has the suffix as the domain
125 atoms := strings.Split(email, "@")
126 if (strings.HasPrefix(domain, ".") && strings.HasSuffix(atoms[len(atoms)-1], domain)) ||
127 (strings.HasPrefix(domain, "*.") && strings.HasSuffix(atoms[len(atoms)-1], domain[1:])) {
128 return true
129 }
130 }
132 return false