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
6e9218bf
Commit
6e9218bf
authored
Mar 20, 2018
by
ale
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add code for writing changed resources to LDAP
parent
a614e880
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
434 additions
and
3 deletions
+434
-3
backend/diff.go
backend/diff.go
+82
-0
backend/model.go
backend/model.go
+187
-0
backend/model_test.go
backend/model_test.go
+79
-0
server/server.go
server/server.go
+45
-3
types.go
types.go
+41
-0
No files found.
backend/diff.go
0 → 100644
View file @
6e9218bf
package
backend
// Implementing read-modify-update cycles with the LDAP backend.
//
// How can we track modifications to Resources in a backend-independent way? One
// method could be to expose specific methods on the Backend interface for every
// change we might want to make: EmailAddAlias, UserSetPassword, etc., but this
// scales very poorly with the number of attributes and operations. Instead, we
// add methods to de-serialize Resources to LDAP objects (or rather, sequences
// of attributes), so that we can compute differences with the original objects
// and issue the appropriate incremental ModifyRequest.
//
// To do this, GetResource keeps around a copy of the original resource, so when
// calling UpdateResource, the two serializations are compared to obtain a
// ModifyRequest: this way, other LDAP attributes that might be present in the
// database (but are not managed by this system) are left untouched on existing
// objects. Attributes explicitly unset (set to the nil value) in the Resource
// will be deleted from LDAP.
import
(
ldap
"gopkg.in/ldap.v2"
"git.autistici.org/ai3/accountserver"
)
func
partialAttributesToMap
(
attrs
[]
ldap
.
PartialAttribute
)
map
[
string
]
ldap
.
PartialAttribute
{
m
:=
make
(
map
[
string
]
ldap
.
PartialAttribute
)
for
_
,
attr
:=
range
attrs
{
m
[
attr
.
Type
]
=
attr
}
return
m
}
func
partialAttributeEquals
(
a
,
b
ldap
.
PartialAttribute
)
bool
{
// We never sort lists, so we can compare them element-wise.
if
len
(
a
.
Vals
)
!=
len
(
b
.
Vals
)
{
return
false
}
for
i
:=
0
;
i
<
len
(
a
.
Vals
);
i
++
{
if
a
.
Vals
[
i
]
!=
b
.
Vals
[
i
]
{
return
false
}
}
return
true
}
// Populate the ldap.ModifyRequest, returns false if unchanged.
func
partialAttributeMapDiff
(
mod
*
ldap
.
ModifyRequest
,
a
,
b
map
[
string
]
ldap
.
PartialAttribute
)
bool
{
var
changed
bool
for
bkey
,
battr
:=
range
b
{
aattr
,
ok
:=
a
[
bkey
]
if
!
ok
{
mod
.
Add
(
battr
.
Type
,
battr
.
Vals
)
changed
=
true
}
else
if
battr
.
Vals
==
nil
{
mod
.
Delete
(
battr
.
Type
,
battr
.
Vals
)
changed
=
true
}
else
if
!
partialAttributeEquals
(
aattr
,
battr
)
{
mod
.
Replace
(
battr
.
Type
,
battr
.
Vals
)
changed
=
true
}
}
return
changed
}
func
diffResources
(
mod
*
ldap
.
ModifyRequest
,
a
,
b
*
accountserver
.
Resource
)
bool
{
return
partialAttributeMapDiff
(
mod
,
partialAttributesToMap
(
resourceToLDAP
(
a
)),
partialAttributesToMap
(
resourceToLDAP
(
b
)),
)
}
// Assemble a ldap.ModifyRequest object by checking differences in two
// Resources objects. If the objects are identical, nil is returned.
func
createModifyRequest
(
dn
string
,
a
,
b
*
accountserver
.
Resource
)
*
ldap
.
ModifyRequest
{
mod
:=
ldap
.
NewModifyRequest
(
dn
)
if
diffResources
(
mod
,
a
,
b
)
{
return
mod
}
return
nil
}
backend/model.go
View file @
6e9218bf
...
...
@@ -17,6 +17,8 @@ import (
// testing.
type
ldapConn
interface
{
Search
(
context
.
Context
,
*
ldap
.
SearchRequest
)
(
*
ldap
.
SearchResult
,
error
)
Add
(
context
.
Context
,
*
ldap
.
AddRequest
)
error
Modify
(
context
.
Context
,
*
ldap
.
ModifyRequest
)
error
Close
()
}
...
...
@@ -25,6 +27,7 @@ type LDAPBackend struct {
conn
ldapConn
userQuery
*
queryConfig
userResourceQueries
[]
*
queryConfig
resourceQueries
map
[
string
]
*
queryConfig
}
// NewLDAPBackend initializes an LDAPBackend object with the given LDAP
...
...
@@ -49,6 +52,33 @@ func NewLDAPBackend(pool *ldaputil.ConnectionPool, base string) *LDAPBackend {
Scope
:
"one"
,
}),
},
resourceQueries
:
map
[
string
]
*
queryConfig
{
accountserver
.
ResourceTypeEmail
:
mustCompileQueryConfig
(
&
queryConfig
{
Base
:
"mail=${resource},uid=${user},ou=People,"
+
base
,
Filter
:
"(objectClass=virtualMailUser)"
,
Scope
:
"base"
,
}),
accountserver
.
ResourceTypeWebsite
:
mustCompileQueryConfig
(
&
queryConfig
{
Base
:
"uid=${user},ou=People,"
+
base
,
Filter
:
"(|(&(objectClass=subSite)(alias=${resource}))(&(objectClass=virtualHost)(cn=${resource})))"
,
Scope
:
"one"
,
}),
accountserver
.
ResourceTypeDAV
:
mustCompileQueryConfig
(
&
queryConfig
{
Base
:
"uid=${user},ou=People,"
+
base
,
Filter
:
"(&(objectClass=ftpAccount)(ftpname=${resource}))"
,
Scope
:
"sub"
,
}),
accountserver
.
ResourceTypeDatabase
:
mustCompileQueryConfig
(
&
queryConfig
{
Base
:
"uid=${user},ou=People,"
+
base
,
Filter
:
"(&(objectClass=dbMysql)(dbname=${resource}))"
,
Scope
:
"sub"
,
}),
accountserver
.
ResourceTypeMailingList
:
mustCompileQueryConfig
(
&
queryConfig
{
Base
:
"ou=Lists,"
+
base
,
Filter
:
"(&(objectClass=mailingList)(listName=${resource}))"
,
Scope
:
"one"
,
}),
},
}
}
...
...
@@ -115,6 +145,13 @@ func s2b(s string) bool {
}
}
func
b2s
(
b
bool
)
string
{
if
b
{
return
"yes"
}
return
"no"
}
func
newResourceFromLDAP
(
entry
*
ldap
.
Entry
,
resourceType
,
nameAttr
string
)
*
accountserver
.
Resource
{
name
:=
entry
.
GetAttributeValue
(
nameAttr
)
return
&
accountserver
.
Resource
{
...
...
@@ -127,6 +164,44 @@ func newResourceFromLDAP(entry *ldap.Entry, resourceType, nameAttr string) *acco
}
}
// Convert a string to a []string with a single item, or nil if the
// string is empty. Useful for optional single-valued LDAP attributes.
func
s2l
(
s
string
)
[]
string
{
if
s
==
""
{
return
nil
}
return
[]
string
{
s
}
}
func
resourceToLDAP
(
r
*
accountserver
.
Resource
)
[]
ldap
.
PartialAttribute
{
// Assemble LDAP attributes for this resource. Use a type-specific
// method to get attributes, then add the resource-generic ones if
// necessary. Note that it is very important that the "objectClass"
// attribute is returned first, or ldap.Add will fail.
var
attrs
[]
ldap
.
PartialAttribute
switch
r
.
Type
{
case
accountserver
.
ResourceTypeEmail
:
attrs
=
emailResourceToLDAP
(
r
)
case
accountserver
.
ResourceTypeWebsite
:
attrs
=
websiteResourceToLDAP
(
r
)
case
accountserver
.
ResourceTypeDAV
:
attrs
=
webDAVResourceToLDAP
(
r
)
case
accountserver
.
ResourceTypeDatabase
:
attrs
=
databaseResourceToLDAP
(
r
)
case
accountserver
.
ResourceTypeMailingList
:
attrs
=
mailingListResourceToLDAP
(
r
)
}
attrs
=
append
(
attrs
,
[]
ldap
.
PartialAttribute
{
{
Type
:
"status"
,
Vals
:
s2l
(
r
.
Status
)},
{
Type
:
"host"
,
Vals
:
s2l
(
r
.
Shard
)},
{
Type
:
"originalHost"
,
Vals
:
s2l
(
r
.
OriginalShard
)},
}
...
)
return
attrs
}
func
newEmailResource
(
entry
*
ldap
.
Entry
)
(
*
accountserver
.
Resource
,
error
)
{
r
:=
newResourceFromLDAP
(
entry
,
accountserver
.
ResourceTypeEmail
,
"mail"
)
r
.
Email
=
&
accountserver
.
Email
{
...
...
@@ -136,6 +211,15 @@ func newEmailResource(entry *ldap.Entry) (*accountserver.Resource, error) {
return
r
,
nil
}
func
emailResourceToLDAP
(
r
*
accountserver
.
Resource
)
[]
ldap
.
PartialAttribute
{
return
[]
ldap
.
PartialAttribute
{
{
Type
:
"objectClass"
,
Vals
:
[]
string
{
"top"
,
"virtualMailUser"
}},
{
Type
:
"mail"
,
Vals
:
s2l
(
r
.
Name
)},
{
Type
:
"mailAlternateAddr"
,
Vals
:
r
.
Email
.
Aliases
},
{
Type
:
"mailMessageStore"
,
Vals
:
s2l
(
r
.
Email
.
Maildir
)},
}
}
func
newMailingListResource
(
entry
*
ldap
.
Entry
)
(
*
accountserver
.
Resource
,
error
)
{
r
:=
newResourceFromLDAP
(
entry
,
accountserver
.
ResourceTypeMailingList
,
"listName"
)
r
.
List
=
&
accountserver
.
MailingList
{
...
...
@@ -145,6 +229,15 @@ func newMailingListResource(entry *ldap.Entry) (*accountserver.Resource, error)
return
r
,
nil
}
func
mailingListResourceToLDAP
(
r
*
accountserver
.
Resource
)
[]
ldap
.
PartialAttribute
{
return
[]
ldap
.
PartialAttribute
{
{
Type
:
"objectClass"
,
Vals
:
[]
string
{
"top"
,
"mailingList"
}},
{
Type
:
"listName"
,
Vals
:
s2l
(
r
.
Name
)},
{
Type
:
"public"
,
Vals
:
s2l
(
b2s
(
r
.
List
.
Public
))},
{
Type
:
"listOwner"
,
Vals
:
r
.
List
.
Admins
},
}
}
func
newWebDAVResource
(
entry
*
ldap
.
Entry
)
(
*
accountserver
.
Resource
,
error
)
{
r
:=
newResourceFromLDAP
(
entry
,
accountserver
.
ResourceTypeDAV
,
"ftpname"
)
r
.
DAV
=
&
accountserver
.
WebDAV
{
...
...
@@ -153,6 +246,14 @@ func newWebDAVResource(entry *ldap.Entry) (*accountserver.Resource, error) {
return
r
,
nil
}
func
webDAVResourceToLDAP
(
r
*
accountserver
.
Resource
)
[]
ldap
.
PartialAttribute
{
return
[]
ldap
.
PartialAttribute
{
{
Type
:
"objectClass"
,
Vals
:
[]
string
{
"top"
,
"person"
,
"posixAccount"
,
"shadowAccount"
,
"organizationalPerson"
,
"inetOrgPerson"
,
"ftpAccount"
}},
{
Type
:
"ftpname"
,
Vals
:
s2l
(
r
.
Name
)},
{
Type
:
"homeDirectory"
,
Vals
:
s2l
(
r
.
DAV
.
Homedir
)},
}
}
func
newWebsiteResource
(
entry
*
ldap
.
Entry
)
(
*
accountserver
.
Resource
,
error
)
{
var
r
*
accountserver
.
Resource
if
isObjectClass
(
entry
,
"subSite"
)
{
...
...
@@ -174,6 +275,25 @@ func newWebsiteResource(entry *ldap.Entry) (*accountserver.Resource, error) {
return
r
,
nil
}
func
websiteResourceToLDAP
(
r
*
accountserver
.
Resource
)
[]
ldap
.
PartialAttribute
{
// Subsites and vhosts have a different RDN.
var
mainRDN
,
mainOC
string
if
strings
.
Contains
(
r
.
Website
.
DisplayName
,
"/"
)
{
mainRDN
=
"alias"
mainOC
=
"subSite"
}
else
{
mainRDN
=
"cn"
mainOC
=
"virtualHost"
}
return
[]
ldap
.
PartialAttribute
{
{
Type
:
"objectClass"
,
Vals
:
[]
string
{
"top"
,
mainOC
}},
{
Type
:
mainRDN
,
Vals
:
s2l
(
r
.
Name
)},
{
Type
:
"option"
,
Vals
:
r
.
Website
.
Options
},
{
Type
:
"documentRoot"
,
Vals
:
s2l
(
r
.
Website
.
DocumentRoot
)},
{
Type
:
"acceptMail"
,
Vals
:
s2l
(
b2s
(
r
.
Website
.
AcceptMail
))},
}
}
func
newDatabaseResource
(
entry
*
ldap
.
Entry
)
(
*
accountserver
.
Resource
,
error
)
{
r
:=
newResourceFromLDAP
(
entry
,
accountserver
.
ResourceTypeDatabase
,
"dbname"
)
r
.
Database
=
&
accountserver
.
Database
{
...
...
@@ -191,6 +311,15 @@ func newDatabaseResource(entry *ldap.Entry) (*accountserver.Resource, error) {
return
r
,
nil
}
func
databaseResourceToLDAP
(
r
*
accountserver
.
Resource
)
[]
ldap
.
PartialAttribute
{
return
[]
ldap
.
PartialAttribute
{
{
Type
:
"objectClass"
,
Vals
:
[]
string
{
"top"
,
"dbMysql"
}},
{
Type
:
"dbname"
,
Vals
:
s2l
(
r
.
Name
)},
{
Type
:
"dbuser"
,
Vals
:
s2l
(
r
.
Database
.
DBUser
)},
{
Type
:
"clearPassword"
,
Vals
:
s2l
(
r
.
Database
.
CleartextPassword
)},
}
}
func
newUser
(
entry
*
ldap
.
Entry
)
(
*
accountserver
.
User
,
error
)
{
user
:=
&
accountserver
.
User
{
Name
:
entry
.
GetAttributeValue
(
"uid"
),
...
...
@@ -251,6 +380,64 @@ func (b *LDAPBackend) GetUser(ctx context.Context, username string) (*accountser
return
user
,
nil
}
func
parseResourceID
(
resourceID
string
)
(
string
,
string
)
{
parts
:=
strings
.
SplitN
(
resourceID
,
"/"
,
2
)
return
parts
[
0
],
parts
[
1
]
}
// GetResource returns a ResourceWrapper, as part of a read-modify-update transaction.
func
(
b
*
LDAPBackend
)
GetResource
(
ctx
context
.
Context
,
username
,
resourceID
string
)
(
*
accountserver
.
Resource
,
error
)
{
resourceType
,
resourceName
:=
parseResourceID
(
resourceID
)
query
,
ok
:=
b
.
resourceQueries
[
resourceType
]
if
!
ok
{
return
nil
,
errors
.
New
(
"unsupported resource type"
)
}
result
,
err
:=
b
.
conn
.
Search
(
ctx
,
query
.
searchRequest
(
map
[
string
]
string
{
"user"
:
username
,
"resource"
:
resourceName
,
"type"
:
resourceType
,
},
nil
))
if
err
!=
nil
{
if
ldap
.
IsErrorWithCode
(
err
,
ldap
.
LDAPResultNoSuchObject
)
{
return
nil
,
nil
}
return
nil
,
err
}
r
,
err
:=
parseLdapResource
(
result
.
Entries
[
0
])
if
err
!=
nil
{
return
nil
,
err
}
r
.
SetBackendHandle
(
&
ldapObjectData
{
dn
:
result
.
Entries
[
0
]
.
DN
,
original
:
r
.
Copy
(),
})
return
r
,
nil
}
// UpdateResource updates a LDAP-backed resource that was obtained by a previous GetResource call.
func
(
b
*
LDAPBackend
)
UpdateResource
(
ctx
context
.
Context
,
username
string
,
r
*
accountserver
.
Resource
)
error
{
lo
,
ok
:=
r
.
GetBackendHandle
()
.
(
*
ldapObjectData
)
if
!
ok
||
lo
==
nil
{
return
errors
.
New
(
"resource did not come from GetResource"
)
}
modRequest
:=
createModifyRequest
(
lo
.
dn
,
lo
.
original
,
r
)
if
modRequest
==
nil
{
return
nil
}
return
b
.
conn
.
Modify
(
ctx
,
modRequest
)
}
type
ldapObjectData
struct
{
dn
string
original
*
accountserver
.
Resource
}
func
parseLdapResource
(
entry
*
ldap
.
Entry
)
(
*
accountserver
.
Resource
,
error
)
{
switch
{
case
isObjectClass
(
entry
,
"virtualMailUser"
)
:
...
...
backend/model_test.go
0 → 100644
View file @
6e9218bf
package
backend
import
(
"reflect"
"testing"
"git.autistici.org/ai3/accountserver"
ldap
"gopkg.in/ldap.v2"
)
func
TestEmailResource_FromLDAP
(
t
*
testing
.
T
)
{
entry
:=
ldap
.
NewEntry
(
"mail=test@investici.org,ou=People,dc=investici,dc=org,o=Anarchy"
,
map
[
string
][]
string
{
"objectClass"
:
[]
string
{
"top"
,
"virtualMailUser"
},
"mail"
:
[]
string
{
"test@investici.org"
},
"status"
:
[]
string
{
"active"
},
"host"
:
[]
string
{
"host1"
},
"originalHost"
:
[]
string
{
"host1"
},
"mailAlternateAddr"
:
[]
string
{
"test2@investici.org"
,
"test3@investici.org"
},
"mailMessageStore"
:
[]
string
{
"test/store"
},
},
)
r
,
err
:=
parseLdapResource
(
entry
)
if
err
!=
nil
{
t
.
Fatal
(
err
)
}
expected
:=
&
accountserver
.
Resource
{
ID
:
"email/test@investici.org"
,
Name
:
"test@investici.org"
,
Type
:
accountserver
.
ResourceTypeEmail
,
Status
:
"active"
,
Shard
:
"host1"
,
OriginalShard
:
"host1"
,
Email
:
&
accountserver
.
Email
{
Aliases
:
[]
string
{
"test2@investici.org"
,
"test3@investici.org"
},
Maildir
:
"test/store"
,
},
}
if
!
reflect
.
DeepEqual
(
r
,
expected
)
{
t
.
Fatalf
(
"bad result: got %+v, expected %+v"
,
r
,
expected
)
}
}
func
TestEmailResource_Diff
(
t
*
testing
.
T
)
{
entry
:=
ldap
.
NewEntry
(
"mail=test@investici.org,ou=People,dc=investici,dc=org,o=Anarchy"
,
map
[
string
][]
string
{
"objectClass"
:
[]
string
{
"top"
,
"virtualMailUser"
},
"mail"
:
[]
string
{
"test@investici.org"
},
"status"
:
[]
string
{
"active"
},
"host"
:
[]
string
{
"host1"
},
"originalHost"
:
[]
string
{
"host1"
},
"mailAlternateAddr"
:
[]
string
{
"test2@investici.org"
,
"test3@investici.org"
},
"mailMessageStore"
:
[]
string
{
"test/store"
},
},
)
r
,
err
:=
parseLdapResource
(
entry
)
if
err
!=
nil
{
t
.
Fatal
(
err
)
}
r2
:=
new
(
accountserver
.
Resource
)
*
r2
=
*
r
r2
.
Shard
=
"host2"
mod
:=
createModifyRequest
(
"dn"
,
r
,
r2
)
if
len
(
mod
.
ReplaceAttributes
)
!=
1
{
t
.
Fatalf
(
"bad ModifyRequest after changing shard: %+v"
,
mod
)
}
r2
.
Email
.
Aliases
=
nil
mod
=
createModifyRequest
(
"dn"
,
r
,
r2
)
if
len
(
mod
.
DeleteAttributes
)
!=
1
{
t
.
Fatalf
(
"bad ModifyRequest after deleting aliases: %+v"
,
mod
)
}
}
server/server.go
View file @
6e9218bf
...
...
@@ -5,6 +5,7 @@ import (
"log"
"net/http"
"git.autistici.org/ai/go-sso"
"git.autistici.org/ai3/go-common/serverutil"
"git.autistici.org/ai3/accountserver"
...
...
@@ -16,11 +17,49 @@ type Backend interface {
}
type
AccountServer
struct
{
backend
Backend
backend
Backend
validator
sso
.
Validator
}
func
New
(
backend
Backend
)
*
AccountServer
{
return
&
AccountServer
{
backend
:
backend
}
func
New
(
backend
Backend
,
ssoPublicKey
[]
byte
,
ssoDomain
string
)
(
*
AccountServer
,
error
)
{
v
,
err
:=
sso
.
NewValidator
(
ssoPublicKey
,
ssoDomain
)
if
err
!=
nil
{
return
nil
,
err
}
return
&
AccountServer
{
backend
:
backend
,
validator
:
v
,
},
nil
}
var
adminGroup
=
"admins"
func
isAdmin
(
tkt
*
sso
.
Ticket
)
bool
{
for
_
,
g
:=
range
tkt
.
Groups
{
if
g
==
adminGroup
{
return
true
}
}
return
false
}
func
(
s
*
AccountServer
)
authorize
(
w
http
.
ResponseWriter
,
ssoToken
,
username
string
)
bool
{
tkt
,
err
:=
s
.
validator
.
Validate
(
ssoToken
,
""
,
s
.
ssoService
,
s
.
ssoGroups
)
if
err
!=
nil
{
log
.
Printf
(
"authentication error: %v"
,
err
)
http
.
Error
(
w
,
err
.
Error
(),
http
.
StatusUnauthorized
)
return
false
}
// Requests are allowed if the SSO ticket corresponds to an admin, or if
// it identifies the same user that we're querying.
if
!
isAdmin
(
tkt
)
&&
tkt
.
User
!=
username
{
log
.
Printf
(
"unauthorized access to %s from %s"
,
username
,
tkt
.
User
)
http
.
Error
(
w
,
err
.
Error
(),
http
.
StatusUnauthorized
)
return
false
}
return
true
}
func
(
s
*
AccountServer
)
handleGetUser
(
w
http
.
ResponseWriter
,
r
*
http
.
Request
)
{
...
...
@@ -28,6 +67,9 @@ func (s *AccountServer) handleGetUser(w http.ResponseWriter, r *http.Request) {
if
!
serverutil
.
DecodeJSONRequest
(
w
,
r
,
&
req
)
{
return
}
if
!
s
.
authorize
(
w
,
req
.
SSO
,
req
.
Username
)
{
return
}
user
,
err
:=
s
.
backend
.
GetUser
(
r
.
Context
(),
req
.
Username
)
if
err
!=
nil
{
...
...
types.go
View file @
6e9218bf
...
...
@@ -89,6 +89,46 @@ type Resource struct {
Website
*
Website
`json:"website,omitempty"`
DAV
*
WebDAV
`json:"dav,omitempty"`
Database
*
Database
`json:"database,omitempty"`
// When the resource is used internally in the accountserver,
// it needs a reference to backend-specific data. This is not
// part of the public interface, and it is not serialized.
opaque
interface
{}
}
// SetBackendHandle associates some backend-specific data at runtime
// with this resource.
func
(
r
*
Resource
)
SetBackendHandle
(
h
interface
{})
{
r
.
opaque
=
h
}
// GetBackendHandle returns the backend-specific data associated with
// the resource.
func
(
r
*
Resource
)
GetBackendHandle
()
interface
{}
{
return
r
.
opaque
}
// Copy the resource (makes a deep copy).
func
(
r
*
Resource
)
Copy
()
*
Resource
{
rr
:=
*
r
switch
{
case
r
.
Email
!=
nil
:
e
:=
*
r
.
Email
rr
.
Email
=
&
e
case
r
.
Website
!=
nil
:
w
:=
*
r
.
Website
rr
.
Website
=
&
w
case
r
.
List
!=
nil
:
l
:=
*
r
.
List
rr
.
List
=
&
l
case
r
.
DAV
!=
nil
:
d
:=
*
r
.
DAV
rr
.
DAV
=
&
d
case
r
.
Database
!=
nil
:
d
:=
*
r
.
Database
rr
.
Database
=
&
d
}
return
&
rr
}
// Email resource attributes.
...
...
@@ -155,6 +195,7 @@ type Blog struct {
// RPC requests.
type
GetUserRequest
struct
{
SSO
string
`json:"sso"`
Username
string
`json:"username"`
}
...
...
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