Skip to content
Snippets Groups Projects
Commit cc4053f8 authored by ale's avatar ale
Browse files

Implement basic audit logging and resource creation

parent 8bf80917
No related branches found
No related tags found
1 merge request!1Txn
...@@ -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)
}
}
audit.go 0 → 100644
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)
}
...@@ -9,6 +9,9 @@ import ( ...@@ -9,6 +9,9 @@ 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"`
ForbiddenUsernamesFile string `yaml:"forbidden_usernames_file"`
ForbiddenPasswords []string `yaml:"forbidden_passwords"`
ForbiddenPasswordsFile string `yaml:"forbidden_passwords_file"`
AvailableDomains map[string][]string `yaml:"available_domains"` AvailableDomains map[string][]string `yaml:"available_domains"`
SSO struct { SSO struct {
...@@ -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)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment