blob: 60697915aa3185ed924e3be4145d6a2ce24a2d46 [file] [log] [blame]
package allowed
import (
"fmt"
"io/ioutil"
"sort"
"strings"
"sync"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/go/util"
fsnotify "gopkg.in/fsnotify.v1"
)
// Allow is used to enforce additional restrictions on who has access to a site,
// eg. members of a group.
type Allow interface {
// Member returns true if the given email address has access.
Member(email string) bool
Emails() []string
}
// AllowedFromList controls access by checking an email address
// against a list of approved domain names and email addresses.
//
// It implements Allow.
type AllowedFromList struct {
domains map[string]bool
emails map[string]bool
}
// NewAllowedFromList creates a new *AllowedFromList from the list of domain names
// and email addresses.
//
// Example:
// a := NewAllowedFromList([]string{"google.com", "chromium.org", "someone@example.org"})
//
func NewAllowedFromList(emailsAndDomains []string) *AllowedFromList {
domains := map[string]bool{}
emails := map[string]bool{}
for _, entry := range emailsAndDomains {
trimmed := strings.ToLower(strings.TrimSpace(entry))
if trimmed == "" {
continue
}
if strings.Contains(trimmed, "@") {
emails[trimmed] = true
} else {
domains[trimmed] = true
}
}
return &AllowedFromList{
domains: domains,
emails: emails,
}
}
// Member returns true if the given email address is AllowedFromList.
func (a *AllowedFromList) Member(email string) bool {
parts := strings.Split(email, "@")
if len(parts) != 2 {
return false
}
if parts[1] == "" {
return false
}
if a.domains[parts[1]] || a.emails[email] {
return true
}
return false
}
func (a *AllowedFromList) Emails() []string {
ret := make([]string, 0, len(a.emails))
for k := range a.emails {
ret = append(ret, k)
}
return ret
}
// Googlers creates a new AllowedFromList which restricts to only users logged
// in with an @google.com account.
func Googlers() *AllowedFromList {
return NewAllowedFromList([]string{"google.com"})
}
// AllowedFromFile implements Allow by reading the list of emails and domains
// from a file. The file is watched for changes and re-read when they occur.
// The file format is one email address or domain name per line.
//
// It implements Allow.
type AllowedFromFile struct {
filename string
allowed *AllowedFromList
mutex sync.RWMutex
}
// NewAllowedFromFile creates a new *AllowedFromFile from the given filename.
//
// Example:
//
// emails := `fred@example.com
// barney@example.com
// chromium.org`
// ioutil.WriteFile("/etc/my_app/auth", []byte(emails), 0644)
// a := NewAllowedFromFile("/etc/my_app/auth")
//
// The presumption is that an AllowedFromFile will be created
// at startup and if creation fails then the application will
// not start.kk
func NewAllowedFromFile(filename string) (*AllowedFromFile, error) {
watcher, err := fsnotify.NewWatcher()
if err != nil {
return nil, fmt.Errorf("Failed to create watcher: %s", err)
}
a := &AllowedFromFile{
filename: filename,
}
if err := a.reload(); err != nil {
util.Close(watcher)
return nil, fmt.Errorf("Failed to initially load allowed from file %q: %s", filename, err)
}
go func() {
for {
select {
case <-watcher.Events:
if err := a.reload(); err != nil {
sklog.Errorf("Failed to reload allowed file %q: %s", filename, err)
}
case err := <-watcher.Errors:
sklog.Errorf("Watcher error %q: %s", filename, err)
}
}
}()
if err := watcher.Add(filename); err != nil {
util.Close(watcher)
return nil, fmt.Errorf("Failed to watch Allowed file %q: %s", filename, err)
}
return a, nil
}
func (a *AllowedFromFile) reload() error {
b, err := ioutil.ReadFile(a.filename)
if err != nil {
return err
}
newAllowed := NewAllowedFromList(strings.Split(string(b), "\n"))
a.mutex.Lock()
defer a.mutex.Unlock()
a.allowed = newAllowed
return nil
}
func (a *AllowedFromFile) Member(email string) bool {
a.mutex.RLock()
defer a.mutex.RUnlock()
return a.allowed.Member(email)
}
func (a *AllowedFromFile) Emails() []string {
a.mutex.RLock()
defer a.mutex.RUnlock()
return a.allowed.Emails()
}
// Union is an Allow which includes members of multiple other Allows.
type Union []Allow
func UnionOf(allows ...Allow) Allow {
return Union(allows)
}
func (allows Union) Member(email string) bool {
for _, a := range allows {
if a.Member(email) {
return true
}
}
return false
}
func (allows Union) Emails() []string {
emails := util.StringSet{}
for _, a := range allows {
emails.AddLists(a.Emails())
}
rv := emails.Keys()
sort.Strings(rv)
return rv
}