Commit e98a25d1 authored by ale's avatar ale
Browse files

Shelve off temporary changes to a separate branch

parent b0fd19b7
Pipeline #6351 failed with stages
in 49 seconds
......@@ -3,6 +3,7 @@ package ldapbackend
import (
"bytes"
"context"
"errors"
"fmt"
"math/rand"
"strconv"
......@@ -525,6 +526,22 @@ func (tx *backendTX) searchResourcesByType(ctx context.Context, pattern, resourc
return out, nil
}
// FindResource fetches a specific resource by type and name.
func (tx *backendTX) FindResource(ctx context.Context, spec as.FindResourceRequest) (*as.RawResource, error) {
result, err := tx.searchResourcesByType(ctx, spec.Name, spec.Type)
if err != nil {
return nil, err
}
switch len(result) {
case 0:
return nil, nil
case 1:
return result[0], nil
default:
return nil, errors.New("too many results")
}
}
// SearchResource returns all the resources matching the pattern.
func (tx *backendTX) SearchResource(ctx context.Context, pattern string) ([]*as.RawResource, error) {
// Aggregate results for all known resource types.
......
......@@ -51,6 +51,7 @@ type TX interface {
UpdateResource(context.Context, *Resource) error
CreateResources(context.Context, *User, []*Resource) ([]*Resource, error)
SetResourcePassword(context.Context, *Resource, string) error
FindResource(context.Context, FindResourceRequest) (*RawResource, error)
HasAnyResource(context.Context, []FindResourceRequest) (bool, error)
GetUser(context.Context, string) (*RawUser, error)
......
package accountserver
import (
"context"
"errors"
"fmt"
"math/rand"
"path/filepath"
"strings"
"time"
)
// Templating, in this context, means filling up derived attributes in a resource.
//
// Derived attributes are always enforced (i.e. we don't check if the
// attribute already has a value), but currently this is only done at
// resource creation time. The plan is to extend this to cover any
// Update call as well.
type templateContext struct {
shards shardBackend
webroot string
}
func (c *templateContext) pickShard(ctx context.Context, r *Resource) (string, error) {
avail := c.shards.GetAvailableShards(ctx, r.Type)
if len(avail) == 0 {
return "", fmt.Errorf("no available shards for resource type %s", r.Type)
}
return avail[rand.Intn(len(avail))], nil
}
func (c *templateContext) setResourceShard(ctx context.Context, r *Resource, ref *Resource) error {
if r.Shard == "" {
if ref != nil {
r.Shard = ref.Shard
} else {
s, err := c.pickShard(ctx, r)
if err != nil {
return err
}
r.Shard = s
}
}
if r.OriginalShard == "" {
r.OriginalShard = r.Shard
}
return nil
}
func (c *templateContext) setResourceStatus(r *Resource) {
if r.Status == "" {
r.Status = ResourceStatusActive
}
}
func (c *templateContext) setCommonResourceAttrs(ctx context.Context, r *Resource, ref *Resource, user *User) error {
// If we reference another resource, ensure it has been templated.
if ref != nil {
if err := c.applyTemplate(ctx, ref, user); err != nil {
return err
}
}
r.CreatedAt = time.Now().UTC().Format("2006-01-02")
c.setResourceStatus(r)
return c.setResourceShard(ctx, r, ref)
}
// Apply default values to an Email resource.
func (c *templateContext) emailResourceTemplate(ctx context.Context, r *Resource, _ *User) error {
// Force the email address to lowercase.
r.Name = strings.ToLower(r.Name)
if r.Email == nil {
r.Email = new(Email)
}
addrParts := strings.Split(r.Name, "@")
if len(addrParts) != 2 {
return errors.New("malformed name")
}
r.Email.Maildir = fmt.Sprintf("%s/%s", addrParts[1], addrParts[0])
r.Email.QuotaLimit = 4096
return c.setCommonResourceAttrs(ctx, r, nil, nil)
}
// Apply default values to a Website or Domain resource.
func (c *templateContext) websiteResourceTemplate(ctx context.Context, r *Resource, user *User) error {
if user == nil {
return errors.New("website resource needs owner")
}
// Force the website address to lowercase.
r.Name = strings.ToLower(r.Name)
if r.Website == nil {
r.Website = new(Website)
}
// If the client did not specify a DocumentRoot, find a DAV resource
// and associate the website with it.
if r.Website.DocumentRoot == "" {
dav := user.GetSingleResourceByType(ResourceTypeDAV)
if dav == nil {
return errors.New("user has no DAV accounts")
}
// The DAV resource may not have been templatized yet.
if dav.DAV == nil || dav.DAV.Homedir == "" {
if err := c.davResourceTemplate(ctx, dav, user); err != nil {
return err
}
}
r.Website.DocumentRoot = filepath.Join(dav.DAV.Homedir, "html-"+r.Name)
}
r.Website.DocumentRoot = filepath.Clean(r.Website.DocumentRoot)
if len(r.Website.Options) == 0 {
r.Website.Options = []string{"nomail"}
}
r.Website.UID = user.UID
dav := findMatchingDAVAccount(user, r)
if dav == nil {
return fmt.Errorf("no DAV resources matching website %s", r.String())
}
return c.setCommonResourceAttrs(ctx, r, dav, user)
}
// Apply default values to a DAV resource.
func (c *templateContext) davResourceTemplate(ctx context.Context, r *Resource, user *User) error {
if user == nil {
return errors.New("dav resource needs owner")
}
// Force the account name to lowercase.
r.Name = strings.ToLower(r.Name)
if r.DAV == nil {
r.DAV = new(WebDAV)
}
if r.DAV.Homedir == "" {
r.DAV.Homedir = filepath.Join(c.webroot, r.Name)
}
r.DAV.Homedir = filepath.Clean(r.DAV.Homedir)
r.DAV.UID = user.UID
return c.setCommonResourceAttrs(ctx, r, nil, user)
}
// Apply default values to a Database resource.
func (c *templateContext) databaseResourceTemplate(ctx context.Context, r *Resource, user *User) error {
if user == nil {
return errors.New("database resource needs owner")
}
// Force the database name to lowercase.
r.Name = strings.ToLower(r.Name)
if r.Database == nil {
r.Database = new(Database)
}
if r.Database.DBUser == "" {
r.Database.DBUser = r.Name
}
return c.setCommonResourceAttrs(ctx, r, user.GetResourceByID(r.ParentID), user)
}
// Apply default values to a MailingList resource.
func (c *templateContext) listResourceTemplate(ctx context.Context, r *Resource, user *User) error {
// Force the list address to lowercase.
r.Name = strings.ToLower(r.Name)
if r.List == nil {
r.List = new(MailingList)
}
// As a convenience, if a user is passed in the context, we add it to
// the list admins.
if user != nil && len(r.List.Admins) == 0 {
r.List.Admins = []string{user.Name}
}
return c.setCommonResourceAttrs(ctx, r, nil, nil)
}
// Apply default values to a Newsletter resource.
func (c *templateContext) newsletterResourceTemplate(ctx context.Context, r *Resource, user *User) error {
// Force the list address to lowercase.
r.Name = strings.ToLower(r.Name)
if r.Newsletter == nil {
r.Newsletter = new(Newsletter)
}
// As a convenience, if a user is passed in the context, we add it to
// the list admins.
if user != nil && len(r.Newsletter.Admins) == 0 {
r.Newsletter.Admins = []string{user.Name}
}
return c.setCommonResourceAttrs(ctx, r, nil, nil)
}
// Apply default values to a resource.
func (c *templateContext) applyTemplate(ctx context.Context, r *Resource, user *User) error {
switch r.Type {
case ResourceTypeEmail:
return c.emailResourceTemplate(ctx, r, user)
case ResourceTypeWebsite, ResourceTypeDomain:
return c.websiteResourceTemplate(ctx, r, user)
case ResourceTypeDAV:
return c.davResourceTemplate(ctx, r, user)
case ResourceTypeDatabase:
return c.databaseResourceTemplate(ctx, r, user)
case ResourceTypeMailingList:
return c.listResourceTemplate(ctx, r, user)
case ResourceTypeNewsletter:
return c.newsletterResourceTemplate(ctx, r, user)
}
return nil
}
package accountserver
import (
"bufio"
"context"
"os"
"strings"
)
// Read non-empty lines from a file, ignoring comments and
// leading/trailing whitespace.
func loadStringListFromFile(path string) ([]string, error) {
f, err := os.Open(path) // #nosec
if err != nil {
return nil, err
}
defer f.Close() // nolint: errcheck
var list []string
scanner := bufio.NewScanner(f)
for scanner.Scan() {
line := scanner.Text()
if n := strings.Index(line, "#"); n >= 0 {
line = line[:n]
}
line = strings.TrimSpace(line)
if line != "" {
list = append(list, line)
}
}
return list, scanner.Err()
}
// A stringSet is just a list of strings with a quick membership test.
type stringSet struct {
set map[string]struct{}
list []string
}
func newStringSetFromList(list []string) *stringSet {
set := make(map[string]struct{})
for _, s := range list {
set[s] = struct{}{}
}
return &stringSet{set: set, list: list}
}
func newStringSetFromFileAndList(list []string, path string) (*stringSet, error) {
if path != "" {
more, err := loadStringListFromFile(path)
if err != nil {
return nil, err
}
list = append(list, more...)
}
return newStringSetFromList(list), nil
}
func (s stringSet) Contains(needle string) bool {
_, ok := s.set[needle]
return ok
}
func (s stringSet) Check(_ *RequestContext, value string) bool {
_, ok := s.set[value]
return ok
}
func (s stringSet) List(_ context.Context) []string {
return s.list
}
// List of email domains backed by our database (with ACLs).
type emailDomainsDataset struct {
backend Backend
}
func (s *emailDomainsDataset) Check(rctx *RequestContext, value string) bool {
tx, err := s.backend.NewTransaction()
if err != nil {
// Fail close on database errors.
return false
}
// Ignore the error in FindResource() since we're going to fail close anyway.
// nolint: errcheck
rsrc, _ := tx.FindResource(rctx.Context, FindResourceRequest{Type: ResourceTypeDomain, Name: value})
if rsrc == nil {
return false
}
// Only allow domains that have acceptMail=true IF the
// requesting user is an admin.
//
// TODO: relax the ACL check allowing domain owners to create
// accounts?
if rsrc.Website.AcceptMail && rctx.Auth.IsAdmin {
return true
}
return false
}
func (s *emailDomainsDataset) List(_ context.Context) []string { return nil }
// Combine multiple datasets.
type multiDataset struct {
datasets []validationDataset
}
func (m *multiDataset) Check(rctx *RequestContext, value string) bool {
for _, d := range m.datasets {
if d.Check(rctx, value) {
return true
}
}
return false
}
func (m *multiDataset) List(ctx context.Context) []string {
var out []string
for _, d := range m.datasets {
out = append(out, d.List(ctx)...)
}
return out
}
This diff is collapsed.
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment