Commit cc4053f8 authored by ale's avatar ale
Browse files

Implement basic audit logging and resource creation

parent 8bf80917
...@@ -5,6 +5,8 @@ import ( ...@@ -5,6 +5,8 @@ import (
"crypto/rand" "crypto/rand"
"encoding/base64" "encoding/base64"
"errors" "errors"
"fmt"
"log"
"git.autistici.org/ai3/go-common/pwhash" "git.autistici.org/ai3/go-common/pwhash"
"github.com/pquerna/otp/totp" "github.com/pquerna/otp/totp"
...@@ -97,7 +99,7 @@ func (s *AccountService) setResourceStatus(ctx context.Context, tx TX, r *Resour ...@@ -97,7 +99,7 @@ func (s *AccountService) setResourceStatus(ctx context.Context, tx TX, r *Resour
if err := tx.UpdateResource(ctx, r); err != nil { if err := tx.UpdateResource(ctx, r); err != nil {
return newBackendError(err) return newBackendError(err)
} }
//s.audit.Log(r.ID.User(), authUser, r.ID, fmt.Sprintf("status changed to %s", status), comment) s.audit.Log(ctx, r.ID, fmt.Sprintf("status set to %s", status))
return nil return nil
} }
...@@ -210,9 +212,10 @@ func (s *AccountService) ResetPassword(ctx context.Context, tx TX, req *ResetPas ...@@ -210,9 +212,10 @@ func (s *AccountService) ResetPassword(ctx context.Context, tx TX, req *ResetPas
return s.withRequest(ctx, req, func(ctx context.Context) error { return s.withRequest(ctx, req, func(ctx context.Context) error {
// Disable 2FA. // Disable 2FA.
if err := tx.DeleteUserTOTPSecret(ctx, user); err != nil { if err := s.disable2FA(ctx, tx, user); err != nil {
return err return err
} }
// Reset encryption keys and set the new password. // Reset encryption keys and set the new password.
return s.changeUserPasswordAndResetEncryptionKeys(ctx, tx, user, req.Password) return s.changeUserPasswordAndResetEncryptionKeys(ctx, tx, user, req.Password)
}) })
...@@ -286,6 +289,9 @@ func (s *AccountService) changeUserPasswordAndUpdateEncryptionKeys(ctx context.C ...@@ -286,6 +289,9 @@ func (s *AccountService) changeUserPasswordAndUpdateEncryptionKeys(ctx context.C
if err := tx.SetUserPassword(ctx, user, encPass); err != nil { if err := tx.SetUserPassword(ctx, user, encPass); err != nil {
return newBackendError(err) return newBackendError(err)
} }
s.audit.Log(ctx, ResourceID{}, "password changed")
return nil return nil
} }
...@@ -303,23 +309,28 @@ func (s *AccountService) changeUserPasswordAndResetEncryptionKeys(ctx context.Co ...@@ -303,23 +309,28 @@ func (s *AccountService) changeUserPasswordAndResetEncryptionKeys(ctx context.Co
return newBackendError(err) return newBackendError(err)
} }
// Wipe all 2FA credentials that had corresponding encryption keys as well.
if err := s.wipeApplicationSpecificPasswords(ctx, tx, user); err != nil {
return err
}
// Set the encrypted password attribute on the user (will set it on emails too). // Set the encrypted password attribute on the user (will set it on emails too).
encPass := pwhash.Encrypt(newPassword) encPass := pwhash.Encrypt(newPassword)
if err := tx.SetUserPassword(ctx, user, encPass); err != nil { if err := tx.SetUserPassword(ctx, user, encPass); err != nil {
return newBackendError(err) return newBackendError(err)
} }
s.audit.Log(ctx, ResourceID{}, "password reset")
return nil return nil
} }
func (s *AccountService) wipeApplicationSpecificPasswords(ctx context.Context, tx TX, user *User) error { // Disable 2FA for a user account.
func (s *AccountService) disable2FA(ctx context.Context, tx TX, user *User) error {
// Disable OTP.
if err := tx.DeleteUserTOTPSecret(ctx, user); err != nil {
return newBackendError(err)
}
// Wipe all app-specific passwords.
for _, asp := range user.AppSpecificPasswords { for _, asp := range user.AppSpecificPasswords {
if err := tx.DeleteApplicationSpecificPassword(ctx, user, asp.ID); err != nil { if err := tx.DeleteApplicationSpecificPassword(ctx, user, asp.ID); err != nil {
return err return newBackendError(err)
} }
} }
return nil return nil
...@@ -569,6 +580,9 @@ type AddEmailAliasRequest struct { ...@@ -569,6 +580,9 @@ type AddEmailAliasRequest struct {
// Validate the request. // Validate the request.
func (r *AddEmailAliasRequest) Validate(ctx context.Context, s *AccountService) error { func (r *AddEmailAliasRequest) Validate(ctx context.Context, s *AccountService) error {
if r.ResourceID.Type() != ResourceTypeEmail {
return errors.New("this operation only works on email resources")
}
return s.emailValidator(ctx, r.Addr) return s.emailValidator(ctx, r.Addr)
} }
...@@ -580,9 +594,6 @@ func (s *AccountService) AddEmailAlias(ctx context.Context, tx TX, req *AddEmail ...@@ -580,9 +594,6 @@ func (s *AccountService) AddEmailAlias(ctx context.Context, tx TX, req *AddEmail
if err != nil { if err != nil {
return err 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 { return s.withRequest(ctx, req, func(ctx context.Context) error {
// Allow at most 5 aliases. // Allow at most 5 aliases.
...@@ -591,7 +602,12 @@ func (s *AccountService) AddEmailAlias(ctx context.Context, tx TX, req *AddEmail ...@@ -591,7 +602,12 @@ func (s *AccountService) AddEmailAlias(ctx context.Context, tx TX, req *AddEmail
} }
r.Email.Aliases = append(r.Email.Aliases, req.Addr) r.Email.Aliases = append(r.Email.Aliases, req.Addr)
return tx.UpdateResource(ctx, r) if err := tx.UpdateResource(ctx, r); err != nil {
return err
}
s.audit.Log(ctx, r.ID, fmt.Sprintf("added alias %s", req.Addr))
return nil
}) })
} }
...@@ -601,15 +617,20 @@ type DeleteEmailAliasRequest struct { ...@@ -601,15 +617,20 @@ type DeleteEmailAliasRequest struct {
Addr string `json:"addr"` Addr string `json:"addr"`
} }
// Validate the request.
func (r *DeleteEmailAliasRequest) Validate(ctx context.Context, s *AccountService) error {
if r.ResourceID.Type() != ResourceTypeEmail {
return errors.New("this operation only works on email resources")
}
return nil
}
// DeleteEmailAlias removes an alias from an email resource. // DeleteEmailAlias removes an alias from an email resource.
func (s *AccountService) DeleteEmailAlias(ctx context.Context, tx TX, req *DeleteEmailAliasRequest) error { func (s *AccountService) DeleteEmailAlias(ctx context.Context, tx TX, req *DeleteEmailAliasRequest) error {
ctx, r, err := s.authorizeResource(ctx, tx, req.ResourceRequestBase) ctx, r, err := s.authorizeResource(ctx, tx, req.ResourceRequestBase)
if err != nil { if err != nil {
return err 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 { return s.withRequest(ctx, req, func(ctx context.Context) error {
var aliases []string var aliases []string
...@@ -619,10 +640,50 @@ func (s *AccountService) DeleteEmailAlias(ctx context.Context, tx TX, req *Delet ...@@ -619,10 +640,50 @@ func (s *AccountService) DeleteEmailAlias(ctx context.Context, tx TX, req *Delet
} }
} }
r.Email.Aliases = aliases r.Email.Aliases = aliases
return tx.UpdateResource(ctx, r) if err := tx.UpdateResource(ctx, r); err != nil {
return err
}
s.audit.Log(ctx, r.ID, fmt.Sprintf("removed alias %s", req.Addr))
return nil
}) })
} }
// CreateResourcesRequest is the request type for AccountService.CreateResources().
type CreateResourcesRequest struct {
SSO string `json:"sso"`
Resources []*Resource `json:"resources"`
}
// CreateResourcesResponse is the response type for AccountService.CreateResources().
type CreateResourcesResponse struct {
IDs []ResourceID `json:"ids"`
}
// CreateResources can create one or more resources.
func (s *AccountService) CreateResources(ctx context.Context, tx TX, req *CreateResourcesRequest) (*CreateResourcesResponse, error) {
ctx, err := s.authorizeAdminGeneric(ctx, tx, req.SSO)
if err != nil {
return nil, err
}
var resp CreateResourcesResponse
err = s.withRequest(ctx, req, func(ctx context.Context) error {
for _, r := range req.Resources {
if err := s.resourceValidator.validateResource(ctx, r); err != nil {
log.Printf("validation error while creating resource %+v: %v", r, err)
return err
}
if err := tx.UpdateResource(ctx, r); err != nil {
return err
}
resp.IDs = append(resp.IDs, r.ID)
}
return nil
})
return &resp, err
}
const appSpecificPasswordLen = 64 const appSpecificPasswordLen = 64
func randomBase64(n int) string { func randomBase64(n int) string {
......
...@@ -147,7 +147,7 @@ func testConfig() *Config { ...@@ -147,7 +147,7 @@ func testConfig() *Config {
func testService(admin string) (*AccountService, TX) { func testService(admin string) (*AccountService, TX) {
be := createFakeBackend() be := createFakeBackend()
svc := newAccountServiceWithSSO(be, testConfig(), &fakeValidator{admin}) svc, _ := newAccountServiceWithSSO(be, testConfig(), &fakeValidator{admin})
tx, _ := be.NewTransaction() tx, _ := be.NewTransaction()
return svc, tx return svc, tx
} }
...@@ -203,7 +203,7 @@ func TestService_Auth(t *testing.T) { ...@@ -203,7 +203,7 @@ func TestService_Auth(t *testing.T) {
func TestService_ChangePassword(t *testing.T) { func TestService_ChangePassword(t *testing.T) {
fb := createFakeBackend() fb := createFakeBackend()
tx, _ := fb.NewTransaction() tx, _ := fb.NewTransaction()
svc := newAccountServiceWithSSO(fb, testConfig(), &fakeValidator{}) svc, _ := newAccountServiceWithSSO(fb, testConfig(), &fakeValidator{})
testdata := []struct { testdata := []struct {
password string password string
...@@ -250,7 +250,7 @@ func TestService_ChangePassword(t *testing.T) { ...@@ -250,7 +250,7 @@ func TestService_ChangePassword(t *testing.T) {
// directly. // directly.
func TestService_EncryptionKeys(t *testing.T) { func TestService_EncryptionKeys(t *testing.T) {
fb := createFakeBackend() fb := createFakeBackend()
svc := newAccountServiceWithSSO(fb, testConfig(), &fakeValidator{}) svc, _ := newAccountServiceWithSSO(fb, testConfig(), &fakeValidator{})
tx, _ := fb.NewTransaction() tx, _ := fb.NewTransaction()
ctx := context.Background() ctx := context.Background()
...@@ -306,3 +306,27 @@ func TestService_AddEmailAlias(t *testing.T) { ...@@ -306,3 +306,27 @@ func TestService_AddEmailAlias(t *testing.T) {
} }
} }
} }
func TestService_Create(t *testing.T) {
svc, tx := testService("")
_, err := svc.CreateResources(context.Background(), tx, &CreateResourcesRequest{
Resources: []*Resource{
&Resource{
ID: NewResourceID(ResourceTypeDomain, "testuser", "example2.com"),
Name: "example2.com",
Status: ResourceStatusActive,
Shard: "host2",
OriginalShard: "host2",
Website: &Website{
URL: "https://example2.com",
DocumentRoot: "/home/sites/example2.com",
AcceptMail: true,
},
},
},
})
if err != nil {
t.Fatal("CreateResources", err)
}
}
package accountserver
import (
"context"
"encoding/json"
"log"
)
type auditLogger interface {
Log(context.Context, ResourceID, string)
}
type auditLogEntry struct {
User string `json:"user,omitempty"`
By string `json:"by"`
Message string `json:"message"`
Comment string `json:"comment,omitempty"`
ResourceName string `json:"resource_name,omitempty"`
ResourceType string `json:"resource_type,omitempty"`
}
type syslogAuditLogger struct{}
func (l *syslogAuditLogger) Log(ctx context.Context, resourceID ResourceID, what string) {
e := auditLogEntry{
User: userFromContext(ctx),
By: authUserFromContext(ctx),
Message: what,
Comment: commentFromContext(ctx),
}
if !resourceID.Empty() {
e.ResourceName = resourceID.Name()
e.ResourceType = resourceID.Type()
}
data, _ := json.Marshal(&e)
log.Printf("@cee:%s", data)
}
...@@ -8,8 +8,11 @@ import ( ...@@ -8,8 +8,11 @@ import (
// Config holds the configuration for the AccountService. // Config holds the configuration for the AccountService.
type Config struct { type Config struct {
ForbiddenUsernames []string `yaml:"forbidden_usernames"` ForbiddenUsernames []string `yaml:"forbidden_usernames"`
AvailableDomains map[string][]string `yaml:"available_domains"` ForbiddenUsernamesFile string `yaml:"forbidden_usernames_file"`
ForbiddenPasswords []string `yaml:"forbidden_passwords"`
ForbiddenPasswordsFile string `yaml:"forbidden_passwords_file"`
AvailableDomains map[string][]string `yaml:"available_domains"`
SSO struct { SSO struct {
PublicKeyFile string `yaml:"public_key"` PublicKeyFile string `yaml:"public_key"`
...@@ -28,13 +31,23 @@ func (c *Config) domainBackend() domainBackend { ...@@ -28,13 +31,23 @@ func (c *Config) domainBackend() domainBackend {
return b return b
} }
func (c *Config) validationConfig() *validationConfig { func (c *Config) validationConfig(be Backend) (*validationConfig, error) {
fu, err := newStringSetFromFileOrList(c.ForbiddenUsernames, c.ForbiddenUsernamesFile)
if err != nil {
return nil, err
}
fp, err := newStringSetFromFileOrList(c.ForbiddenPasswords, c.ForbiddenPasswordsFile)
if err != nil {
return nil, err
}
return &validationConfig{ return &validationConfig{
forbiddenUsernames: newStringSetFromList(c.ForbiddenUsernames), forbiddenUsernames: fu,
forbiddenPasswords: newStringSetFromList([]string{"123456", "password", "password1"}), forbiddenPasswords: fp,
minPasswordLength: 6, minPasswordLength: 6,
maxPasswordLength: 128, maxPasswordLength: 128,
} domains: c.domainBackend(),
backend: be,
}, nil
} }
func (c *Config) ssoValidator() (sso.Validator, error) { func (c *Config) ssoValidator() (sso.Validator, error) {
......
...@@ -68,8 +68,12 @@ type AccountService struct { ...@@ -68,8 +68,12 @@ type AccountService struct {
ssoGroups []string ssoGroups []string
ssoAdminGroup string ssoAdminGroup string
audit auditLogger
passwordValidator ValidatorFunc passwordValidator ValidatorFunc
emailValidator ValidatorFunc emailValidator ValidatorFunc
listValidator ValidatorFunc
resourceValidator *resourceValidator
} }
// NewAccountService builds a new AccountService with the specified configuration. // NewAccountService builds a new AccountService with the specified configuration.
...@@ -79,23 +83,28 @@ func NewAccountService(backend Backend, config *Config) (*AccountService, error) ...@@ -79,23 +83,28 @@ func NewAccountService(backend Backend, config *Config) (*AccountService, error)
return nil, err return nil, err
} }
return newAccountServiceWithSSO(backend, config, ssoValidator), nil return newAccountServiceWithSSO(backend, config, ssoValidator)
} }
func newAccountServiceWithSSO(backend Backend, config *Config, ssoValidator sso.Validator) *AccountService { func newAccountServiceWithSSO(backend Backend, config *Config, ssoValidator sso.Validator) (*AccountService, error) {
s := &AccountService{ s := &AccountService{
validator: ssoValidator, validator: ssoValidator,
ssoService: config.SSO.Service, ssoService: config.SSO.Service,
ssoGroups: config.SSO.Groups, ssoGroups: config.SSO.Groups,
ssoAdminGroup: config.SSO.AdminGroup, ssoAdminGroup: config.SSO.AdminGroup,
audit: &syslogAuditLogger{},
} }
validationConfig := config.validationConfig() validationConfig, err := config.validationConfig(backend)
domainBackend := config.domainBackend() if err != nil {
return nil, err
}
s.passwordValidator = validPassword(validationConfig) s.passwordValidator = validPassword(validationConfig)
s.emailValidator = validHostedEmail(validationConfig, domainBackend, backend) s.emailValidator = validHostedEmail(validationConfig)
s.listValidator = validHostedMailingList(validationConfig)
s.resourceValidator = newResourceValidator(validationConfig)
return s return s, nil
} }
func (s *AccountService) isAdmin(tkt *sso.Ticket) bool { func (s *AccountService) isAdmin(tkt *sso.Ticket) bool {
...@@ -145,6 +154,23 @@ func authUserFromContext(ctx context.Context) string { ...@@ -145,6 +154,23 @@ func authUserFromContext(ctx context.Context) string {
return "" return ""
} }
func (s *AccountService) authorizeAdminGeneric(ctx context.Context, tx TX, ssoTicket string) (context.Context, error) {
// Validate the SSO ticket.
tkt, err := s.validateSSO(ssoTicket)
if err != nil {
return nil, newAuthError(err)
}
// Requests are allowed if the SSO ticket corresponds to an admin, or if
// it identifies the same user that we're querying.
if !s.isAdmin(tkt) {
return nil, newAuthError(ErrUnauthorized)
}
ctx = context.WithValue(ctx, authUserCtxKey, tkt.User)
return ctx, nil
}
func (s *AccountService) authorizeAdmin(ctx context.Context, tx TX, req RequestBase) (context.Context, *User, error) { func (s *AccountService) authorizeAdmin(ctx context.Context, tx TX, req RequestBase) (context.Context, *User, error) {
// Validate the SSO ticket. // Validate the SSO ticket.
tkt, err := s.validateSSO(req.SSO) tkt, err := s.validateSSO(req.SSO)
......
Supports Markdown
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