Skip to content
GitLab
Projects
Groups
Snippets
Help
Loading...
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
A
accountserver
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
2
Issues
2
List
Boards
Labels
Service Desk
Milestones
Merge Requests
1
Merge Requests
1
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Operations
Operations
Incidents
Environments
Analytics
Analytics
CI / CD
Repository
Value Stream
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
ai3
accountserver
Commits
cc4053f8
Commit
cc4053f8
authored
Jun 24, 2018
by
ale
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Implement basic audit logging and resource creation
parent
8bf80917
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
195 additions
and
32 deletions
+195
-32
actions.go
actions.go
+78
-17
actions_test.go
actions_test.go
+27
-3
audit.go
audit.go
+39
-0
config.go
config.go
+19
-6
service.go
service.go
+32
-6
No files found.
actions.go
View file @
cc4053f8
...
...
@@ -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
(
c
tx
,
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
{
...
...
actions_test.go
View file @
cc4053f8
...
...
@@ -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
)
}
}
audit.go
0 → 100644
View file @
cc4053f8
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
)
}
config.go
View file @
cc4053f8
...
...
@@ -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
)
{
...
...
service.go
View file @
cc4053f8
...
...
@@ -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
)
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
.
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment