Skip to content
GitLab
Menu
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Menu
Open sidebar
ai3
accountserver
Commits
2fd20609
Commit
2fd20609
authored
Jun 25, 2018
by
ale
Browse files
Implement resource templates
Let the server fill in default values for resource and user creation.
parent
e8b91a57
Changes
6
Hide whitespace changes
Inline
Side-by-side
actions.go
View file @
2fd20609
...
...
@@ -662,6 +662,13 @@ type CreateResourcesResponse struct {
Resources
[]
*
Resource
`json:"resources"`
}
// ApplyTemplate fills in default values for the resources in the request.
func
(
req
*
CreateResourcesRequest
)
ApplyTemplate
(
ctx
context
.
Context
,
s
*
AccountService
,
user
*
User
)
{
for
_
,
r
:=
range
req
.
Resources
{
s
.
resourceTemplates
.
applyTemplate
(
ctx
,
r
,
user
)
}
}
// Validate the request.
func
(
req
*
CreateResourcesRequest
)
Validate
(
ctx
context
.
Context
,
s
*
AccountService
,
user
*
User
)
error
{
var
owner
string
...
...
@@ -743,6 +750,13 @@ type CreateUserRequest struct {
User
*
User
`json:"user"`
}
// ApplyTemplate fills in default values for the resources in the request.
func
(
req
*
CreateUserRequest
)
ApplyTemplate
(
ctx
context
.
Context
,
s
*
AccountService
,
user
*
User
)
{
for
_
,
r
:=
range
req
.
User
.
Resources
{
s
.
resourceTemplates
.
applyTemplate
(
ctx
,
r
,
user
)
}
}
// Validate the request.
func
(
req
*
CreateUserRequest
)
Validate
(
ctx
context
.
Context
,
s
*
AccountService
,
user
*
User
)
error
{
// Override server-generated values.
...
...
config.go
View file @
2fd20609
...
...
@@ -13,6 +13,7 @@ type Config struct {
ForbiddenPasswords
[]
string
`yaml:"forbidden_passwords"`
ForbiddenPasswordsFile
string
`yaml:"forbidden_passwords_file"`
AvailableDomains
map
[
string
][]
string
`yaml:"available_domains"`
WebsiteRootDir
string
`yaml:"website_root_dir"`
Shards
struct
{
Available
map
[
string
][]
string
`yaml:"available"`
...
...
@@ -65,12 +66,20 @@ func (c *Config) validationContext(be Backend) (*validationContext, error) {
forbiddenPasswords
:
fp
,
minPasswordLength
:
6
,
maxPasswordLength
:
128
,
webroot
:
c
.
WebsiteRootDir
,
domains
:
c
.
domainBackend
(),
shards
:
c
.
shardBackend
(),
backend
:
be
,
},
nil
}
func
(
c
*
Config
)
templateContext
()
*
templateContext
{
return
&
templateContext
{
shards
:
c
.
shardBackend
(),
webroot
:
c
.
WebsiteRootDir
,
}
}
func
(
c
*
Config
)
ssoValidator
()
(
sso
.
Validator
,
error
)
{
pkey
,
err
:=
ioutil
.
ReadFile
(
c
.
SSO
.
PublicKeyFile
)
if
err
!=
nil
{
...
...
integrationtest/integration_test.go
View file @
2fd20609
...
...
@@ -123,13 +123,16 @@ func startService(t testing.TB) (func(), *testClient) {
svcConfig
.
AvailableDomains
=
map
[
string
][]
string
{
accountserver
.
ResourceTypeEmail
:
[]
string
{
"example.com"
},
}
shards
:=
[]
string
{
"host1"
,
"host2"
,
"host3"
}
svcConfig
.
Shards
.
Available
=
map
[
string
][]
string
{
accountserver
.
ResourceTypeEmail
:
[]
string
{
"host1"
,
"host2"
,
"host3"
},
accountserver
.
ResourceTypeWebsite
:
[]
string
{
"host1"
,
"host2"
,
"host3"
},
accountserver
.
ResourceTypeDomain
:
[]
string
{
"host1"
,
"host2"
,
"host3"
},
accountserver
.
ResourceTypeDAV
:
[]
string
{
"host1"
,
"host2"
,
"host3"
},
accountserver
.
ResourceTypeEmail
:
shards
,
accountserver
.
ResourceTypeWebsite
:
shards
,
accountserver
.
ResourceTypeDomain
:
shards
,
accountserver
.
ResourceTypeDAV
:
shards
,
accountserver
.
ResourceTypeDatabase
:
shards
,
}
svcConfig
.
Shards
.
Allowed
=
svcConfig
.
Shards
.
Available
svcConfig
.
WebsiteRootDir
=
"/home/users/investici.org"
service
,
err
:=
accountserver
.
NewAccountService
(
be
,
&
svcConfig
)
if
err
!=
nil
{
...
...
@@ -286,7 +289,7 @@ func TestIntegration_CreateResource(t *testing.T) {
false
,
},
//
Malformed website metadata (empty document root)
.
//
Empty document root will be fixed by templating
.
{
&
accountserver
.
Resource
{
ID
:
accountserver
.
NewResourceID
(
accountserver
.
ResourceTypeDomain
,
"uno@investici.org"
,
"example3.com"
),
...
...
@@ -294,11 +297,9 @@ func TestIntegration_CreateResource(t *testing.T) {
Status
:
accountserver
.
ResourceStatusActive
,
Shard
:
"host2"
,
OriginalShard
:
"host2"
,
Website
:
&
accountserver
.
Website
{
URL
:
"https://example3.com"
,
},
Website
:
&
accountserver
.
Website
{},
},
fals
e
,
tru
e
,
},
// Malformed resource metadata (name fails validation).
...
...
@@ -321,13 +322,13 @@ func TestIntegration_CreateResource(t *testing.T) {
{
&
accountserver
.
Resource
{
ID
:
accountserver
.
NewResourceID
(
accountserver
.
ResourceTypeDomain
,
"uno@investici.org"
,
"example3.com"
),
Name
:
"example
3
.com"
,
Name
:
"example
4
.com"
,
Status
:
accountserver
.
ResourceStatusActive
,
Shard
:
"zebra"
,
OriginalShard
:
"zebra"
,
Website
:
&
accountserver
.
Website
{
URL
:
"https://example
3
.com"
,
DocumentRoot
:
"/home/users/investici.org/uno/html-example
3
.com"
,
URL
:
"https://example
4
.com"
,
DocumentRoot
:
"/home/users/investici.org/uno/html-example
4
.com"
,
},
},
false
,
...
...
@@ -337,13 +338,13 @@ func TestIntegration_CreateResource(t *testing.T) {
{
&
accountserver
.
Resource
{
ID
:
accountserver
.
NewResourceID
(
accountserver
.
ResourceTypeDomain
,
"uno@investici.org"
,
"example3.com"
),
Name
:
"example
3
.com"
,
Name
:
"example
5
.com"
,
Status
:
accountserver
.
ResourceStatusActive
,
Shard
:
"host2"
,
OriginalShard
:
"host2"
,
Website
:
&
accountserver
.
Website
{
URL
:
"https://example
3
.com"
,
DocumentRoot
:
"/
foo/bar/example3.com
"
,
URL
:
"https://example
5
.com"
,
DocumentRoot
:
"/
home/users/investici.org/nonexisting
"
,
},
},
false
,
...
...
@@ -363,33 +364,27 @@ func TestIntegration_CreateResource(t *testing.T) {
}
}
func
TestIntegration_CreateMultipleResources
(
t
*
testing
.
T
)
{
func
TestIntegration_CreateMultipleResources
_WithTemplate
(
t
*
testing
.
T
)
{
stop
,
c
:=
startService
(
t
)
defer
stop
()
// The create request is very bare, most values will be filled
// in by the server using resource templates.
err
:=
c
.
request
(
"/api/resource/create"
,
&
accountserver
.
CreateResourcesRequest
{
SSO
:
c
.
ssoTicket
(
testAdminUser
),
Resources
:
[]
*
accountserver
.
Resource
{
&
accountserver
.
Resource
{
ID
:
accountserver
.
NewResourceID
(
accountserver
.
ResourceTypeDomain
,
"uno@investici.org"
,
"example3.com"
),
Name
:
"example3.com"
,
Status
:
accountserver
.
ResourceStatusActive
,
Shard
:
"host2"
,
OriginalShard
:
"host2"
,
Website
:
&
accountserver
.
Website
{
URL
:
"https://example3.com"
,
DocumentRoot
:
"/foo/bar/example3.com"
,
},
ID
:
accountserver
.
NewResourceID
(
accountserver
.
ResourceTypeDomain
,
"uno@investici.org"
,
"example3.com"
),
Name
:
"example3.com"
,
},
&
accountserver
.
Resource
{
ID
:
accountserver
.
NewResourceID
(
accountserver
.
ResourceTypeDAV
,
"uno@investici.org"
,
"example3dav"
),
Name
:
"example3dav"
,
Status
:
accountserver
.
ResourceStatusActive
,
Shard
:
"host2"
,
OriginalShard
:
"host2"
,
DAV
:
&
accountserver
.
WebDAV
{
Homedir
:
"/foo/bar"
,
},
ID
:
accountserver
.
NewResourceID
(
accountserver
.
ResourceTypeDAV
,
"uno@investici.org"
,
"example3dav"
),
Name
:
"example3dav"
,
},
&
accountserver
.
Resource
{
ID
:
accountserver
.
NewResourceID
(
accountserver
.
ResourceTypeDatabase
,
"uno@investici.org"
,
"cn=example3.com"
,
"example3"
),
ParentID
:
accountserver
.
NewResourceID
(
accountserver
.
ResourceTypeDomain
,
"uno@investici.org"
,
"example3.com"
),
Name
:
"example3"
,
},
},
},
nil
)
...
...
service.go
View file @
2fd20609
...
...
@@ -74,6 +74,7 @@ type AccountService struct {
fieldValidators
*
fieldValidators
resourceValidator
*
resourceValidator
userValidator
UserValidatorFunc
resourceTemplates
*
templateContext
}
// NewAccountService builds a new AccountService with the specified configuration.
...
...
@@ -103,6 +104,8 @@ func newAccountServiceWithSSO(backend Backend, config *Config, ssoValidator sso.
s
.
resourceValidator
=
newResourceValidator
(
vc
)
s
.
userValidator
=
vc
.
validUser
()
s
.
resourceTemplates
=
config
.
templateContext
()
return
s
,
nil
}
...
...
@@ -261,6 +264,10 @@ type hasNewContext interface {
NewContext
(
context
.
Context
)
context
.
Context
}
type
hasApplyTemplate
interface
{
ApplyTemplate
(
context
.
Context
,
*
AccountService
,
*
User
)
}
type
hasValidate
interface
{
Validate
(
context
.
Context
,
*
AccountService
)
error
}
...
...
@@ -273,9 +280,24 @@ type hasCompoundValidate interface {
// (mostly in the Context, used for later logging). The user
// parameter, if present, is passed to the Validate request method.
func
(
s
*
AccountService
)
withRequest
(
ctx
context
.
Context
,
req
interface
{},
user
*
User
,
f
func
(
context
.
Context
)
error
)
error
{
// If the request has a NewContext() method, call it to obtain
// a request-specific context (this step usually adds
// parameters for logging).
if
rnc
,
ok
:=
req
.
(
hasNewContext
);
ok
{
ctx
=
rnc
.
NewContext
(
ctx
)
}
// Apply a template to the request, to fill in default values
// etc., if the request has an ApplyTemplate() method.
if
rt
,
ok
:=
req
.
(
hasApplyTemplate
);
ok
{
rt
.
ApplyTemplate
(
ctx
,
s
,
user
)
}
// If the request has a Validate() method, validate the
// request. We support two different fingerprints for the
// Validate() method, one without, and the other with a *User
// argument ("compound Validate"), for resource-level
// validators.
if
rv
,
ok
:=
req
.
(
hasValidate
);
ok
{
if
err
:=
rv
.
Validate
(
ctx
,
s
);
err
!=
nil
{
return
newRequestError
(
err
)
...
...
types.go
View file @
2fd20609
...
...
@@ -86,6 +86,8 @@ type AppSpecificPasswordInfo struct {
Comment
string
`json:"comment"`
}
// Well-known user encryption key types, corresponding to primary and
// secondary passwords.
const
(
UserEncryptionKeyMainID
=
"main"
UserEncryptionKeyRecoveryID
=
"recovery"
...
...
@@ -98,6 +100,7 @@ type UserEncryptionKey struct {
Key
[]
byte
`json:"key"`
}
// Resource types.
const
(
ResourceTypeEmail
=
"email"
ResourceTypeMailingList
=
"list"
...
...
@@ -107,6 +110,7 @@ const (
ResourceTypeDatabase
=
"db"
)
// Resource status values.
const
(
ResourceStatusActive
=
"active"
ResourceStatusInactive
=
"inactive"
...
...
@@ -300,7 +304,7 @@ type WebDAV struct {
// Website resource attributes.
type
Website
struct
{
URL
string
`json:"url"`
URL
string
`json:"url
,omitempty
"`
ParentDomain
string
`json:"parent_domain,omitempty"`
AcceptMail
bool
`json:"accept_mail"`
Options
[]
string
`json:"options,omitempty"`
...
...
validators.go
View file @
2fd20609
...
...
@@ -5,7 +5,10 @@ import (
"context"
"errors"
"fmt"
"log"
"math/rand"
"os"
"path/filepath"
"regexp"
"strings"
...
...
@@ -35,6 +38,7 @@ type validationContext struct {
forbiddenPasswords
stringSet
minPasswordLength
int
maxPasswordLength
int
webroot
string
domains
domainBackend
shards
shardBackend
backend
Backend
...
...
@@ -462,13 +466,17 @@ func (v *validationContext) validListResource() ResourceValidatorFunc {
}
}
func
has
MatchingDAVAccount
(
user
*
User
,
r
*
Resource
)
bool
{
func
find
MatchingDAVAccount
(
user
*
User
,
r
*
Resource
)
*
Resource
{
for
_
,
dav
:=
range
user
.
GetResourcesByType
(
ResourceTypeDAV
)
{
if
strings
.
HasPrefix
(
r
.
Website
.
DocumentRoot
,
dav
.
DAV
.
Homedir
+
"/"
)
{
return
true
if
isSubdir
(
dav
.
DAV
.
Homedir
,
r
.
Website
.
DocumentRoot
)
{
return
r
}
}
return
false
return
nil
}
func
hasMatchingDAVAccount
(
user
*
User
,
r
*
Resource
)
bool
{
return
findMatchingDAVAccount
(
user
,
r
)
!=
nil
}
func
(
v
*
validationContext
)
validDomainResource
()
ResourceValidatorFunc
{
...
...
@@ -502,6 +510,9 @@ func (v *validationContext) validDomainResource() ResourceValidatorFunc {
if
r
.
Website
.
DocumentRoot
==
""
{
return
errors
.
New
(
"empty document_root"
)
}
if
!
isSubdir
(
v
.
webroot
,
r
.
Website
.
DocumentRoot
)
{
return
errors
.
New
(
"document root outside of web root"
)
}
if
!
hasMatchingDAVAccount
(
user
,
r
)
{
return
errors
.
New
(
"website has no matching DAV account"
)
}
...
...
@@ -543,6 +554,9 @@ func (v *validationContext) validWebsiteResource() ResourceValidatorFunc {
if
r
.
Website
.
DocumentRoot
==
""
{
return
errors
.
New
(
"empty document_root"
)
}
if
!
isSubdir
(
v
.
webroot
,
r
.
Website
.
DocumentRoot
)
{
return
errors
.
New
(
"document root outside of web root"
)
}
if
!
hasMatchingDAVAccount
(
user
,
r
)
{
return
errors
.
New
(
"website has no matching DAV account"
)
}
...
...
@@ -564,6 +578,9 @@ func (v *validationContext) validDAVResource() ResourceValidatorFunc {
if
r
.
DAV
==
nil
{
return
errors
.
New
(
"resource has no dav metadata"
)
}
if
!
isSubdir
(
v
.
webroot
,
r
.
DAV
.
Homedir
)
{
return
errors
.
New
(
"homedir outside of web root"
)
}
return
nil
}
}
...
...
@@ -636,3 +653,128 @@ func (v *validationContext) validUser() UserValidatorFunc {
return
nameValidator
(
ctx
,
user
.
Name
)
}
}
// A ResourceTemplateFunc fills up server-generated fields with
// defaults for newly created resources. Called before validation.
type
ResourceTemplateFunc
func
(
context
.
Context
,
*
Resource
,
*
User
)
type
templateContext
struct
{
shards
shardBackend
webroot
string
}
func
(
c
*
templateContext
)
pickShard
(
ctx
context
.
Context
,
r
*
Resource
)
string
{
avail
:=
c
.
shards
.
GetAvailableShards
(
ctx
,
r
.
ID
.
Type
())
if
len
(
avail
)
==
0
{
return
""
}
return
avail
[
rand
.
Intn
(
len
(
avail
))]
}
func
(
c
*
templateContext
)
setResourceShard
(
ctx
context
.
Context
,
r
*
Resource
,
ref
*
Resource
)
{
if
r
.
Shard
==
""
{
if
ref
!=
nil
{
// If we are evaluating templates out of
// order, the reference resource may not have
// a shard yet. Assign it now.
if
ref
.
Shard
==
""
{
ref
.
Shard
=
c
.
pickShard
(
ctx
,
ref
)
}
r
.
Shard
=
ref
.
Shard
}
else
{
r
.
Shard
=
c
.
pickShard
(
ctx
,
r
)
}
}
if
r
.
OriginalShard
==
""
{
r
.
OriginalShard
=
r
.
Shard
}
}
func
(
c
*
templateContext
)
setResourceStatus
(
r
*
Resource
)
{
if
r
.
Status
==
""
{
r
.
Status
=
ResourceStatusActive
}
}
func
(
c
*
templateContext
)
emailResourceTemplate
(
ctx
context
.
Context
,
r
*
Resource
,
user
*
User
)
{
if
r
.
Email
==
nil
{
r
.
Email
=
new
(
Email
)
}
addrParts
:=
strings
.
Split
(
r
.
ID
.
Name
(),
"@"
)
r
.
Email
.
Maildir
=
fmt
.
Sprintf
(
"%s/%s"
,
addrParts
[
1
],
addrParts
[
0
])
r
.
Email
.
QuotaLimit
=
4096
c
.
setResourceShard
(
ctx
,
r
,
nil
)
c
.
setResourceStatus
(
r
)
}
func
(
c
*
templateContext
)
websiteResourceTemplate
(
ctx
context
.
Context
,
r
*
Resource
,
user
*
User
)
{
if
r
.
Website
==
nil
{
r
.
Website
=
new
(
Website
)
}
if
r
.
Website
.
DocumentRoot
==
""
{
if
dav
:=
user
.
GetSingleResourceByType
(
ResourceTypeDAV
);
dav
!=
nil
{
// The DAV resource may not have been templatized yet.
if
dav
.
DAV
==
nil
||
dav
.
DAV
.
Homedir
==
""
{
c
.
davResourceTemplate
(
ctx
,
dav
,
user
)
}
r
.
Website
.
DocumentRoot
=
filepath
.
Join
(
dav
.
DAV
.
Homedir
,
"html-"
+
r
.
ID
.
Name
())
}
}
r
.
Website
.
DocumentRoot
=
filepath
.
Clean
(
r
.
Website
.
DocumentRoot
)
if
len
(
r
.
Website
.
Options
)
==
0
{
r
.
Website
.
Options
=
[]
string
{
"nomail"
}
}
dav
:=
findMatchingDAVAccount
(
user
,
r
)
c
.
setResourceShard
(
ctx
,
r
,
dav
)
c
.
setResourceStatus
(
r
)
log
.
Printf
(
"applyTemplate(%s) -> %+v"
,
r
.
ID
,
r
.
Website
)
}
func
(
c
*
templateContext
)
davResourceTemplate
(
ctx
context
.
Context
,
r
*
Resource
,
user
*
User
)
{
if
r
.
DAV
==
nil
{
r
.
DAV
=
new
(
WebDAV
)
}
if
r
.
DAV
.
Homedir
==
""
{
r
.
DAV
.
Homedir
=
filepath
.
Join
(
c
.
webroot
,
r
.
ID
.
Name
())
}
r
.
DAV
.
Homedir
=
filepath
.
Clean
(
r
.
DAV
.
Homedir
)
c
.
setResourceShard
(
ctx
,
r
,
nil
)
c
.
setResourceStatus
(
r
)
log
.
Printf
(
"applyTemplate(%s) -> %+v"
,
r
.
ID
,
r
.
DAV
)
}
func
(
c
*
templateContext
)
databaseResourceTemplate
(
ctx
context
.
Context
,
r
*
Resource
,
user
*
User
)
{
if
r
.
Database
==
nil
{
r
.
Database
=
new
(
Database
)
}
if
r
.
Database
.
DBUser
==
""
{
r
.
Database
.
DBUser
=
r
.
ID
.
Name
()
}
c
.
setResourceShard
(
ctx
,
r
,
user
.
GetResourceByID
(
r
.
ParentID
))
c
.
setResourceStatus
(
r
)
log
.
Printf
(
"applyTemplate(%s) -> %+v"
,
r
.
ID
,
r
.
Database
)
}
func
(
c
*
templateContext
)
applyTemplate
(
ctx
context
.
Context
,
r
*
Resource
,
user
*
User
)
{
switch
r
.
ID
.
Type
()
{
case
ResourceTypeEmail
:
c
.
emailResourceTemplate
(
ctx
,
r
,
user
)
case
ResourceTypeWebsite
,
ResourceTypeDomain
:
c
.
websiteResourceTemplate
(
ctx
,
r
,
user
)
case
ResourceTypeDAV
:
c
.
davResourceTemplate
(
ctx
,
r
,
user
)
case
ResourceTypeDatabase
:
c
.
databaseResourceTemplate
(
ctx
,
r
,
user
)
}
}
func
isSubdir
(
root
,
dir
string
)
bool
{
return
strings
.
HasPrefix
(
dir
,
root
+
"/"
)
}
Write
Preview
Supports
Markdown
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