Skip to content
GitLab
Explore
Sign in
Primary navigation
Search or go to…
Project
A
accountserver
Manage
Activity
Members
Labels
Plan
Issues
Issue boards
Milestones
Code
Merge requests
Repository
Branches
Commits
Tags
Repository graph
Compare revisions
Build
Pipelines
Jobs
Pipeline schedules
Artifacts
Deploy
Releases
Model registry
Monitor
Service Desk
Analyze
Model experiments
Help
Help
Support
GitLab documentation
Compare GitLab plans
Community forum
Contribute to GitLab
Provide feedback
Keyboard shortcuts
?
Snippets
Groups
Projects
Show more breadcrumbs
ai3
accountserver
Commits
6e9218bf
Commit
6e9218bf
authored
7 years ago
by
ale
Browse files
Options
Downloads
Patches
Plain Diff
Add code for writing changed resources to LDAP
parent
a614e880
No related branches found
No related tags found
No related merge requests found
Changes
5
Show whitespace changes
Inline
Side-by-side
Showing
5 changed files
backend/diff.go
+82
-0
82 additions, 0 deletions
backend/diff.go
backend/model.go
+187
-0
187 additions, 0 deletions
backend/model.go
backend/model_test.go
+79
-0
79 additions, 0 deletions
backend/model_test.go
server/server.go
+45
-3
45 additions, 3 deletions
server/server.go
types.go
+41
-0
41 additions, 0 deletions
types.go
with
434 additions
and
3 deletions
backend/diff.go
0 → 100644
+
82
−
0
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
}
This diff is collapsed.
Click to expand it.
backend/model.go
+
187
−
0
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"
)
:
...
...
This diff is collapsed.
Click to expand it.
backend/model_test.go
0 → 100644
+
79
−
0
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
)
}
}
This diff is collapsed.
Click to expand it.
server/server.go
+
45
−
3
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"
...
...
@@ -17,10 +18,48 @@ type Backend interface {
type
AccountServer
struct
{
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
{
...
...
This diff is collapsed.
Click to expand it.
types.go
+
41
−
0
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"`
}
...
...
This diff is collapsed.
Click to expand it.
Preview
0%
Loading
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Save comment
Cancel
Please
register
or
sign in
to comment