Commit cc4053f8 authored by ale's avatar ale

Implement basic audit logging and resource creation

parent 8bf80917
......@@ -5,6 +5,8 @@ import (
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"log"
"git.autistici.org/ai3/go-common/pwhash"
"github.com/pquerna/otp/totp"
......@@ -97,7 +99,7 @@ func (s *AccountService) setResourceStatus(ctx context.Context, tx TX, r *Resour
if err := tx.UpdateResource(ctx, r); err != nil {
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
}
......@@ -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 {
// Disable 2FA.
if err := tx.DeleteUserTOTPSecret(ctx, user); err != nil {
if err := s.disable2FA(ctx, tx, user); err != nil {
return err
}
// Reset encryption keys and set the new password.
return s.changeUserPasswordAndResetEncryptionKeys(ctx, tx, user, req.Password)
})
......@@ -286,6 +289,9 @@ func (s *AccountService) changeUserPasswordAndUpdateEncryptionKeys(ctx context.C
if err := tx.SetUserPassword(ctx, user, encPass); err != nil {
return newBackendError(err)
}
s.audit.Log(ctx, ResourceID{}, "password changed")
return nil
}
......@@ -303,23 +309,28 @@ func (s *AccountService) changeUserPasswordAndResetEncryptionKeys(ctx context.Co
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).
encPass := pwhash.Encrypt(newPassword)
if err := tx.SetUserPassword(ctx, user, encPass); err != nil {
return newBackendError(err)
}
s.audit.Log(ctx, ResourceID{}, "password reset")
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 {
if err := tx.DeleteApplicationSpecificPassword(ctx, user, asp.ID); err != nil {
return err
return newBackendError(err)
}
}
return nil
......@@ -569,6 +580,9 @@ type AddEmailAliasRequest struct {
// Validate the request.
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)
}
......@@ -580,9 +594,6 @@ func (s *AccountService) AddEmailAlias(ctx context.Context, tx TX, req *AddEmail
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.
......@@ -591,7 +602,12 @@ func (s *AccountService) AddEmailAlias(ctx context.Context, tx TX, req *AddEmail
}
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 {
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.
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
......@@ -619,10 +640,50 @@ func (s *AccountService) DeleteEmailAlias(ctx context.Context, tx TX, req *Delet
}
}
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
func randomBase64(n int) string {
......
......@@ -147,7 +147,7 @@ func testConfig() *Config {
func testService(admin string) (*AccountService, TX) {
be := createFakeBackend()
svc := newAccountServiceWithSSO(be, testConfig(), &fakeValidator{admin})
svc, _ := newAccountServiceWithSSO(be, testConfig(), &fakeValidator{admin})
tx, _ := be.NewTransaction()
return svc, tx
}
......@@ -203,7 +203,7 @@ func TestService_Auth(t *testing.T) {
func TestService_ChangePassword(t *testing.T) {
fb := createFakeBackend()
tx, _ := fb.NewTransaction()
svc := newAccountServiceWithSSO(fb, testConfig(), &fakeValidator{})
svc, _ := newAccountServiceWithSSO(fb, testConfig(), &fakeValidator{})
testdata := []struct {
password string
......@@ -250,7 +250,7 @@ func TestService_ChangePassword(t *testing.T) {
// directly.
func TestService_EncryptionKeys(t *testing.T) {
fb := createFakeBackend()
svc := newAccountServiceWithSSO(fb, testConfig(), &fakeValidator{})
svc, _ := newAccountServiceWithSSO(fb, testConfig(), &fakeValidator{})
tx, _ := fb.NewTransaction()
ctx := context.Background()
......@@ -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 (
// Config holds the configuration for the AccountService.
type Config struct {
ForbiddenUsernames []string `yaml:"forbidden_usernames"`
AvailableDomains map[string][]string `yaml:"available_domains"`
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"`
SSO struct {
PublicKeyFile string `yaml:"public_key"`
......@@ -28,13 +31,23 @@ func (c *Config) domainBackend() domainBackend {
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{
forbiddenUsernames: newStringSetFromList(c.ForbiddenUsernames),
forbiddenPasswords: newStringSetFromList([]string{"123456", "password", "password1"}),
forbiddenUsernames: fu,
forbiddenPasswords: fp,
minPasswordLength: 6,
maxPasswordLength: 128,
}
domains: c.domainBackend(),
backend: be,
}, nil
}
func (c *Config) ssoValidator() (sso.Validator, error) {
......
......@@ -68,8 +68,12 @@ type AccountService struct {
ssoGroups []string
ssoAdminGroup string
audit auditLogger
passwordValidator ValidatorFunc
emailValidator ValidatorFunc
listValidator ValidatorFunc
resourceValidator *resourceValidator
}
// NewAccountService builds a new AccountService with the specified configuration.
......@@ -79,23 +83,28 @@ func NewAccountService(backend Backend, config *Config) (*AccountService, error)
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{
validator: ssoValidator,
ssoService: config.SSO.Service,
ssoGroups: config.SSO.Groups,
ssoAdminGroup: config.SSO.AdminGroup,
audit: &syslogAuditLogger{},
}
validationConfig := config.validationConfig()
domainBackend := config.domainBackend()
validationConfig, err := config.validationConfig(backend)
if err != nil {
return nil, err
}
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 {
......@@ -145,6 +154,23 @@ func authUserFromContext(ctx context.Context) string {
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) {
// Validate the SSO ticket.
tkt, err := s.validateSSO(req.SSO)
......
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