Commit 5f6e6818 authored by ale's avatar ale

Add API to manage email aliases

parent da51a6a2
......@@ -109,7 +109,7 @@ type DisableResourceRequest struct {
// DisableResource disables a resource belonging to the user.
func (s *AccountService) DisableResource(ctx context.Context, tx TX, req *DisableResourceRequest) error {
ctx, r, err := s.authorizeResource(req.NewContext(ctx), tx, req.ResourceRequestBase)
ctx, r, err := s.authorizeResource(ctx, tx, req.ResourceRequestBase)
if err != nil {
return err
}
......@@ -125,7 +125,7 @@ type EnableResourceRequest struct {
// EnableResource enables a resource belonging to the user.
func (s *AccountService) EnableResource(ctx context.Context, tx TX, req *EnableResourceRequest) error {
ctx, r, err := s.authorizeResource(req.NewContext(ctx), tx, req.ResourceRequestBase)
ctx, r, err := s.authorizeResource(ctx, tx, req.ResourceRequestBase)
if err != nil {
return err
}
......@@ -332,7 +332,7 @@ type DeleteApplicationSpecificPasswordRequest struct {
// DeleteApplicationSpecificPassword destroys an application-specific
// password, identified by its unique ID.
func (s *AccountService) DeleteApplicationSpecificPassword(ctx context.Context, tx TX, req *DeleteApplicationSpecificPasswordRequest) error {
ctx, user, err := s.authorizeUser(req.NewContext(ctx), tx, req.RequestBase)
ctx, user, err := s.authorizeUser(ctx, tx, req.RequestBase)
if err != nil {
return err
}
......@@ -379,7 +379,7 @@ type MoveResourceResponse struct {
// once regardless of which individual ResourceID is provided as long
// as it belongs to the group.
func (s *AccountService) MoveResource(ctx context.Context, tx TX, req *MoveResourceRequest) (*MoveResourceResponse, error) {
ctx, user, err := s.authorizeAdmin(req.NewContext(ctx), tx, req.RequestBase)
ctx, user, err := s.authorizeAdmin(ctx, tx, req.RequestBase)
if err != nil {
return nil, err
}
......@@ -437,7 +437,7 @@ type EnableOTPResponse struct {
// or it can let the server generate a new secret by passing an empty
// totp_secret.
func (s *AccountService) EnableOTP(ctx context.Context, tx TX, req *EnableOTPRequest) (*EnableOTPResponse, error) {
ctx, user, err := s.authorizeUser(req.NewContext(ctx), tx, req.RequestBase)
ctx, user, err := s.authorizeUser(ctx, tx, req.RequestBase)
if err != nil {
return nil, err
}
......@@ -469,7 +469,7 @@ type DisableOTPRequest struct {
// DisableOTP disables two-factor authentication for a user.
func (s *AccountService) DisableOTP(ctx context.Context, tx TX, req *DisableOTPRequest) error {
ctx, user, err := s.authorizeUser(req.NewContext(ctx), tx, req.RequestBase)
ctx, user, err := s.authorizeUser(ctx, tx, req.RequestBase)
if err != nil {
return err
}
......@@ -483,6 +483,68 @@ func (s *AccountService) DisableOTP(ctx context.Context, tx TX, req *DisableOTPR
})
}
// AddEmailAliasRequest is the request type for AccountService.AddEmailAlias().
type AddEmailAliasRequest struct {
ResourceRequestBase
Addr string `json:"addr"`
}
// Validate the request.
func (r *AddEmailAliasRequest) Validate(ctx context.Context, s *AccountService) error {
return s.emailValidator(ctx, r.Addr)
}
const maxEmailAliases = 5
// AddEmailAlias adds an alias (additional address) to an email resource.
func (s *AccountService) AddEmailAlias(ctx context.Context, tx TX, req *AddEmailAliasRequest) error {
ctx, r, err := s.authorizeResource(ctx, tx, req.ResourceRequestBase)
if err != nil {
return err
}
if r.ID.Type() != ResourceTypeEmail {
return errors.New("this operation only works on email resources")
}
return s.withRequest(ctx, req, func(ctx context.Context) error {
// Allow at most 5 aliases.
if len(r.Email.Aliases) >= maxEmailAliases {
return errors.New("too many aliases")
}
r.Email.Aliases = append(r.Email.Aliases, req.Addr)
return tx.UpdateResource(ctx, r)
})
}
// DeleteEmailAliasRequest is the request type for AccountService.DeleteEmailAlias().
type DeleteEmailAliasRequest struct {
ResourceRequestBase
Addr string `json:"addr"`
}
// DeleteEmailAlias removes an alias from an email resource.
func (s *AccountService) DeleteEmailAlias(ctx context.Context, tx TX, req *DeleteEmailAliasRequest) error {
ctx, r, err := s.authorizeResource(ctx, tx, req.ResourceRequestBase)
if err != nil {
return err
}
if r.ID.Type() != ResourceTypeEmail {
return errors.New("this operation only works on email resources")
}
return s.withRequest(ctx, req, func(ctx context.Context) error {
var aliases []string
for _, a := range r.Email.Aliases {
if a != req.Addr {
aliases = append(aliases, a)
}
}
r.Email.Aliases = aliases
return tx.UpdateResource(ctx, r)
})
}
const appSpecificPasswordLen = 64
func randomBase64(n int) string {
......
......@@ -99,31 +99,42 @@ func (v *fakeValidator) Validate(tkt, nonce, service string, _ []string) (*sso.T
}, nil
}
func (b *fakeBackend) addUser(user *User) {
b.users[user.Name] = user
b.resources[user.Name] = make(map[string]*Resource)
for _, r := range user.Resources {
b.resources[user.Name][r.ID.String()] = r
}
}
func createFakeBackend() *fakeBackend {
fb := &fakeBackend{
users: map[string]*User{
"testuser": &User{
Name: "testuser",
Resources: []*Resource{
{
ID: NewResourceID("email", "testuser@example.com", "testuser@example.com"),
Name: "testuser@example.com",
Status: ResourceStatusActive,
Email: &Email{},
},
},
},
},
users: make(map[string]*User),
resources: make(map[string]map[string]*Resource),
passwords: make(map[string]string),
appSpecificPasswords: make(map[string][]*AppSpecificPasswordInfo),
encryptionKeys: make(map[string][]*UserEncryptionKey),
}
fb.addUser(&User{
Name: "testuser",
Resources: []*Resource{
{
ID: NewResourceID(ResourceTypeEmail, "testuser", "testuser@example.com"),
Name: "testuser@example.com",
Status: ResourceStatusActive,
Email: &Email{},
},
},
})
return fb
}
func testConfig() *Config {
var c Config
c.ForbiddenUsernames = []string{"root"}
c.AvailableDomains = map[string][]string{
ResourceTypeEmail: []string{"example.com"},
}
c.SSO.Domain = "mydomain"
c.SSO.Service = "service/"
c.SSO.AdminGroup = testAdminGroupName
......@@ -255,3 +266,33 @@ func TestService_EncryptionKeys(t *testing.T) {
t.Fatalf("found %d encryption keys, expected 1", n)
}
}
// Try adding aliases to the email resource.
func TestService_AddEmailAlias(t *testing.T) {
svc, tx := testService("")
testdata := []struct {
addr string
expectedOk bool
}{
{"alias@example.com", true},
{"another-example-address@example.com", true},
{"root@example.com", false},
{"alias@other-domain.com", false},
}
for _, td := range testdata {
req := &AddEmailAliasRequest{
ResourceRequestBase: ResourceRequestBase{
SSO: "testuser",
ResourceID: NewResourceID(ResourceTypeEmail, "testuser", "testuser@example.com"),
},
Addr: td.addr,
}
err := svc.AddEmailAlias(context.TODO(), tx, req)
if err != nil && td.expectedOk {
t.Errorf("AddEmailAlias(%s) failed: %v", td.addr, err)
} else if err == nil && !td.expectedOk {
t.Errorf("AddEmailAlias(%s) did not fail but should have", td.addr)
}
}
}
......@@ -68,8 +68,7 @@ type AccountService struct {
ssoAdminGroup string
passwordValidator ValidatorFunc
dataValidators map[string]ValidatorFunc
//adminDataValidators map[string]ValidatorFunc
emailValidator ValidatorFunc
}
// NewAccountService builds a new AccountService with the specified configuration.
......@@ -92,11 +91,8 @@ func newAccountServiceWithSSO(backend Backend, config *Config, ssoValidator sso.
validationConfig := config.validationConfig()
domainBackend := config.domainBackend()
s.dataValidators = map[string]ValidatorFunc{
ResourceTypeEmail: validHostedEmail(validationConfig, domainBackend, backend),
ResourceTypeMailingList: validHostedMailingList(validationConfig, domainBackend, backend),
}
s.passwordValidator = validPassword(validationConfig)
s.emailValidator = validHostedEmail(validationConfig, domainBackend, backend)
return s
}
......
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