Skip to content
GitLab
Projects
Groups
Snippets
Help
Loading...
Help
What's new
10
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
Open sidebar
ai3
accountserver
Commits
7879718b
Commit
7879718b
authored
Feb 03, 2019
by
ale
Browse files
Options
Browse Files
Download
Plain Diff
Merge branch 'search' into 'master'
Search Closes
#6
See merge request
!4
parents
a3f4990f
dd4cfa28
Pipeline
#2180
failed with stages
in 1 minute and 17 seconds
Changes
10
Pipelines
1
Hide whitespace changes
Inline
Side-by-side
Showing
10 changed files
with
307 additions
and
15 deletions
+307
-15
actions_resource.go
actions_resource.go
+52
-0
actions_test.go
actions_test.go
+10
-0
actions_user.go
actions_user.go
+32
-1
backend/model.go
backend/model.go
+73
-2
backend/model_test.go
backend/model_test.go
+35
-0
backend/resources.go
backend/resources.go
+8
-9
backend/util.go
backend/util.go
+20
-3
integrationtest/search_test.go
integrationtest/search_test.go
+67
-0
server/server.go
server/server.go
+3
-0
service.go
service.go
+7
-0
No files found.
actions_resource.go
View file @
7879718b
...
...
@@ -5,6 +5,57 @@ import (
"fmt"
)
// GetResourceRequest requests a specific resource.
type
GetResourceRequest
struct
{
AdminResourceRequestBase
}
// GetResourceResponse is the response type for GetResourceRequest.
type
GetResourceResponse
struct
{
Resource
*
Resource
`json:"resource"`
Owner
string
`json:"owner"`
}
// Serve the request.
func
(
r
*
GetResourceRequest
)
Serve
(
rctx
*
RequestContext
)
(
interface
{},
error
)
{
resp
:=
GetResourceResponse
{
Resource
:
rctx
.
Resource
,
}
if
rctx
.
User
!=
nil
{
resp
.
Owner
=
rctx
.
User
.
Name
}
return
&
resp
,
nil
}
// SearchResourceRequest searches for resources matching a pattern.
type
SearchResourceRequest
struct
{
AdminRequestBase
Pattern
string
`json:"pattern"`
}
// Validate the request.
func
(
r
*
SearchResourceRequest
)
Validate
(
rctx
*
RequestContext
)
error
{
if
r
.
Pattern
==
""
{
return
errors
.
New
(
"empty pattern"
)
}
return
nil
}
// SearchResourceResponse is the response type for SearchResourceRequest.
type
SearchResourceResponse
struct
{
Results
[]
*
RawResource
`json:"results"`
}
// Serve the request.
func
(
r
*
SearchResourceRequest
)
Serve
(
rctx
*
RequestContext
)
(
interface
{},
error
)
{
results
,
err
:=
rctx
.
TX
.
SearchResource
(
rctx
.
Context
,
r
.
Pattern
)
if
err
!=
nil
{
return
nil
,
err
}
return
&
SearchResourceResponse
{
Results
:
results
},
nil
}
// setResourceStatus sets the status of a single resource (shared
// logic between enable / disable resource methods).
func
setResourceStatus
(
rctx
*
RequestContext
,
status
string
)
error
{
...
...
@@ -100,6 +151,7 @@ type MoveResourceResponse struct {
// Serve the request.
func
(
r
*
MoveResourceRequest
)
Serve
(
rctx
*
RequestContext
)
(
interface
{},
error
)
{
resources
:=
[]
*
Resource
{
rctx
.
Resource
}
// If we have an associated user, collect all related
// resources, as they should all be moved at once.
if
rctx
.
User
!=
nil
&&
rctx
.
Resource
.
Group
!=
""
{
...
...
actions_test.go
View file @
7879718b
...
...
@@ -52,6 +52,16 @@ func (b *fakeBackend) GetUser(_ context.Context, username string) (*RawUser, err
},
nil
}
func
(
b
*
fakeBackend
)
SearchUser
(
_
context
.
Context
,
pattern
string
)
([]
string
,
error
)
{
var
out
[]
string
for
username
:=
range
b
.
users
{
if
strings
.
HasPrefix
(
username
,
pattern
)
{
out
=
append
(
out
,
username
)
}
}
return
out
,
nil
}
func
(
b
*
fakeBackend
)
UpdateUser
(
_
context
.
Context
,
user
*
User
)
error
{
b
.
users
[
user
.
Name
]
=
user
return
nil
...
...
actions_user.go
View file @
7879718b
...
...
@@ -7,7 +7,7 @@ import (
umdb
"git.autistici.org/id/usermetadb"
)
// GetUserRequest
is the request type for GetUserAction
.
// GetUserRequest
retrieves a specific User
.
type
GetUserRequest
struct
{
UserRequestBase
...
...
@@ -25,6 +25,37 @@ func (r *GetUserRequest) Serve(rctx *RequestContext) (interface{}, error) {
return
&
rctx
.
User
.
User
,
nil
}
// SearchUserRequest searches the database for users with names
// matching a given pattern. The actual pattern semantics are
// backend-specific (for LDAP, this is a prefix string search).
type
SearchUserRequest
struct
{
AdminRequestBase
Pattern
string
`json:"pattern"`
}
// Validate the request.
func
(
r
*
SearchUserRequest
)
Validate
(
rctx
*
RequestContext
)
error
{
if
r
.
Pattern
==
""
{
return
errors
.
New
(
"empty pattern"
)
}
return
nil
}
// SearchUserResponse is the response type for SearchUserRequest.
type
SearchUserResponse
struct
{
Usernames
[]
string
`json:"usernames"`
}
// Serve the request.
func
(
r
*
SearchUserRequest
)
Serve
(
rctx
*
RequestContext
)
(
interface
{},
error
)
{
usernames
,
err
:=
rctx
.
TX
.
SearchUser
(
rctx
.
Context
,
r
.
Pattern
)
if
err
!=
nil
{
return
nil
,
err
}
return
&
SearchUserResponse
{
Usernames
:
usernames
},
nil
}
// ChangeUserPasswordRequest updates a user's password. It will also take
// care of re-encrypting the user encryption key, if present.
type
ChangeUserPasswordRequest
struct
{
...
...
backend/model.go
View file @
7879718b
...
...
@@ -39,6 +39,7 @@ type backend struct {
conn
ldapConn
baseDN
string
userQuery
*
queryTemplate
searchUserQuery
*
queryTemplate
userResourceQueries
[]
*
queryTemplate
resources
*
resourceRegistry
...
...
@@ -88,6 +89,11 @@ func newLDAPBackendWithConn(conn ldapConn, baseDN string) (*backend, error) {
Filter
:
"(objectClass=*)"
,
Scope
:
ldap
.
ScopeBaseObject
,
},
searchUserQuery
:
&
queryTemplate
{
Base
:
joinDN
(
"ou=People"
,
baseDN
),
Filter
:
"(uid=${pattern}*)"
,
Scope
:
ldap
.
ScopeSingleLevel
,
},
userResourceQueries
:
[]
*
queryTemplate
{
// Find all resources that are children of the main uid object.
&
queryTemplate
{
...
...
@@ -206,7 +212,7 @@ func (tx *backendTX) UpdateUser(ctx context.Context, user *as.User) error {
// GetUser returns a user.
func
(
tx
*
backendTX
)
GetUser
(
ctx
context
.
Context
,
username
string
)
(
*
as
.
RawUser
,
error
)
{
// First of all, find the main user object, and just that one.
vars
:=
map
[
string
]
string
{
"user"
:
username
}
vars
:=
templateVars
{
"user"
:
username
}
result
,
err
:=
tx
.
search
(
ctx
,
tx
.
backend
.
userQuery
.
query
(
vars
))
if
err
!=
nil
{
if
ldap
.
IsErrorWithCode
(
err
,
ldap
.
LDAPResultNoSuchObject
)
{
...
...
@@ -263,6 +269,24 @@ func (tx *backendTX) GetUser(ctx context.Context, username string) (*as.RawUser,
return
user
,
nil
}
func
(
tx
*
backendTX
)
SearchUser
(
ctx
context
.
Context
,
pattern
string
)
([]
string
,
error
)
{
// First of all, find the main user object, and just that one.
vars
:=
templateVars
{
"pattern"
:
rawVariable
(
pattern
)}
result
,
err
:=
tx
.
search
(
ctx
,
tx
.
backend
.
searchUserQuery
.
query
(
vars
))
if
err
!=
nil
{
return
nil
,
err
}
if
len
(
result
.
Entries
)
==
0
{
return
nil
,
nil
}
var
out
[]
string
for
_
,
entry
:=
range
result
.
Entries
{
out
=
append
(
out
,
entry
.
GetAttributeValue
(
"uid"
))
}
return
out
,
nil
}
func
(
tx
*
backendTX
)
SetUserPassword
(
ctx
context
.
Context
,
user
*
as
.
User
,
encryptedPassword
string
)
(
err
error
)
{
dn
:=
tx
.
getUserDN
(
user
)
tx
.
setAttr
(
dn
,
passwordLDAPAttr
,
encryptedPassword
)
...
...
@@ -373,7 +397,7 @@ func (tx *backendTX) hasResource(ctx context.Context, resourceType, resourceName
// Make a quick LDAP search that only fetches the DN attribute.
tpl
.
Attrs
=
[]
string
{
"dn"
}
result
,
err
:=
tx
.
search
(
ctx
,
tpl
.
query
(
map
[
string
]
string
{
result
,
err
:=
tx
.
search
(
ctx
,
tpl
.
query
(
templateVars
{
"resource"
:
resourceName
,
"type"
:
resourceType
,
}))
...
...
@@ -400,6 +424,53 @@ func (tx *backendTX) HasAnyResource(ctx context.Context, resourceIDs []as.FindRe
return
false
,
nil
}
func
(
tx
*
backendTX
)
searchResourcesByType
(
ctx
context
.
Context
,
pattern
,
resourceType
string
)
([]
*
as
.
RawResource
,
error
)
{
tpl
,
err
:=
tx
.
backend
.
resources
.
SearchQuery
(
resourceType
)
if
err
!=
nil
{
return
nil
,
err
}
result
,
err
:=
tx
.
search
(
ctx
,
tpl
.
query
(
templateVars
{
"resource"
:
rawVariable
(
pattern
),
"type"
:
resourceType
,
}))
if
err
!=
nil
{
if
ldap
.
IsErrorWithCode
(
err
,
ldap
.
LDAPResultNoSuchObject
)
{
return
nil
,
nil
}
return
nil
,
err
}
if
len
(
result
.
Entries
)
==
0
{
return
nil
,
nil
}
var
out
[]
*
as
.
RawResource
for
_
,
entry
:=
range
result
.
Entries
{
rsrc
,
err
:=
tx
.
backend
.
resources
.
FromLDAP
(
entry
)
if
err
!=
nil
{
return
nil
,
err
}
out
=
append
(
out
,
&
as
.
RawResource
{
Resource
:
*
rsrc
,
Owner
:
tx
.
backend
.
resources
.
GetOwner
(
rsrc
),
})
}
return
out
,
nil
}
// SearchResource returns all the resources matching the pattern.
func
(
tx
*
backendTX
)
SearchResource
(
ctx
context
.
Context
,
pattern
string
)
([]
*
as
.
RawResource
,
error
)
{
// Aggregate results for all known resource types.
var
out
[]
*
as
.
RawResource
for
_
,
typ
:=
range
tx
.
backend
.
resources
.
types
{
r
,
err
:=
tx
.
searchResourcesByType
(
ctx
,
pattern
,
typ
)
if
err
!=
nil
{
return
nil
,
err
}
out
=
append
(
out
,
r
...
)
}
return
out
,
nil
}
// GetResource returns a ResourceWrapper, as part of a read-modify-update transaction.
func
(
tx
*
backendTX
)
GetResource
(
ctx
context
.
Context
,
rsrcID
as
.
ResourceID
)
(
*
as
.
RawResource
,
error
)
{
// From the resource ID we can obtain the DN, and fetch it
...
...
backend/model_test.go
View file @
7879718b
...
...
@@ -163,6 +163,22 @@ func TestModel_GetUser_Resources(t *testing.T) {
}
}
func
TestModel_SearchUser
(
t
*
testing
.
T
)
{
stop
,
b
:=
startServer
(
t
)
defer
stop
()
tx
,
_
:=
b
.
NewTransaction
()
users
,
err
:=
tx
.
SearchUser
(
context
.
Background
(),
"uno"
)
if
err
!=
nil
{
t
.
Fatal
(
err
)
}
if
len
(
users
)
!=
1
{
t
.
Fatalf
(
"expected 1 user, got %d"
,
len
(
users
))
}
if
users
[
0
]
!=
testUser1
{
t
.
Fatalf
(
"expected %s, got %s"
,
testUser1
,
users
[
0
])
}
}
func
TestModel_SetResourceStatus
(
t
*
testing
.
T
)
{
stop
:=
ldaptest
.
StartServer
(
t
,
&
ldaptest
.
Config
{
Dir
:
"../ldaptest"
,
...
...
@@ -236,6 +252,25 @@ func TestModel_HasAnyResource(t *testing.T) {
}
}
func
TestModel_SearchResource
(
t
*
testing
.
T
)
{
stop
,
b
:=
startServer
(
t
)
defer
stop
()
for
_
,
pattern
:=
range
[]
string
{
"uno@investici.org"
,
"uno*"
}
{
tx
,
_
:=
b
.
NewTransaction
()
resources
,
err
:=
tx
.
SearchResource
(
context
.
Background
(),
"uno@investici.org"
)
if
err
!=
nil
{
t
.
Fatalf
(
"SearchUser(%s): %v"
,
pattern
,
err
)
}
if
len
(
resources
)
!=
1
{
t
.
Fatalf
(
"SearchUser(%s): expected 1 resource, got %d"
,
pattern
,
len
(
resources
))
}
if
resources
[
0
]
.
Owner
!=
"uno@investici.org"
{
t
.
Fatalf
(
"SearchUser(%s): resource owner is %s, expected uno@investici.org"
,
pattern
,
resources
[
0
]
.
Owner
)
}
}
}
func
TestModel_SetUserPassword
(
t
*
testing
.
T
)
{
stop
,
b
,
user
:=
startServerAndGetUser
(
t
)
defer
stop
()
...
...
backend/resources.go
View file @
7879718b
...
...
@@ -25,6 +25,7 @@ type resourceHandler interface {
// interface to a resourceHandler, with a few exceptions.
type
resourceRegistry
struct
{
handlers
map
[
string
]
resourceHandler
types
[]
string
}
func
newResourceRegistry
()
*
resourceRegistry
{
...
...
@@ -34,10 +35,8 @@ func newResourceRegistry() *resourceRegistry {
}
func
(
reg
*
resourceRegistry
)
register
(
rtype
string
,
h
resourceHandler
)
{
if
reg
.
handlers
==
nil
{
reg
.
handlers
=
make
(
map
[
string
]
resourceHandler
)
}
reg
.
handlers
[
rtype
]
=
h
reg
.
types
=
append
(
reg
.
types
,
rtype
)
}
func
(
reg
*
resourceRegistry
)
dispatch
(
rsrcType
string
,
f
func
(
resourceHandler
)
error
)
error
{
...
...
@@ -124,7 +123,7 @@ func (h *emailResourceHandler) MakeDN(user *as.User, rsrc *as.Resource) (string,
if
user
==
nil
{
return
""
,
errors
.
New
(
"email resource requires an owner"
)
}
rdn
:=
replaceVars
(
"mail=${resource}"
,
map
[
string
]
string
{
"resource"
:
rsrc
.
Name
})
rdn
:=
replaceVars
(
"mail=${resource}"
,
templateVars
{
"resource"
:
rsrc
.
Name
})
return
joinDN
(
rdn
,
getUserDN
(
user
,
h
.
baseDN
)),
nil
}
...
...
@@ -173,7 +172,7 @@ type mailingListResourceHandler struct {
}
func
(
h
*
mailingListResourceHandler
)
MakeDN
(
_
*
as
.
User
,
rsrc
*
as
.
Resource
)
(
string
,
error
)
{
rdn
:=
replaceVars
(
"listName=${resource}"
,
map
[
string
]
string
{
"resource"
:
rsrc
.
Name
})
rdn
:=
replaceVars
(
"listName=${resource}"
,
templateVars
{
"resource"
:
rsrc
.
Name
})
return
joinDN
(
rdn
,
"ou=Lists"
,
h
.
baseDN
),
nil
}
...
...
@@ -225,7 +224,7 @@ func (h *websiteResourceHandler) MakeDN(user *as.User, rsrc *as.Resource) (strin
if
user
==
nil
{
return
""
,
errors
.
New
(
"website resource requires an owner"
)
}
rdn
:=
replaceVars
(
"alias=${resource}"
,
map
[
string
]
string
{
"resource"
:
rsrc
.
Name
})
rdn
:=
replaceVars
(
"alias=${resource}"
,
templateVars
{
"resource"
:
rsrc
.
Name
})
return
joinDN
(
rdn
,
getUserDN
(
user
,
h
.
baseDN
)),
nil
}
...
...
@@ -289,7 +288,7 @@ func (h *domainResourceHandler) MakeDN(user *as.User, rsrc *as.Resource) (string
if
user
==
nil
{
return
""
,
errors
.
New
(
"domain resource requires an owner"
)
}
rdn
:=
replaceVars
(
"cn=${resource}"
,
map
[
string
]
string
{
"resource"
:
rsrc
.
Name
})
rdn
:=
replaceVars
(
"cn=${resource}"
,
templateVars
{
"resource"
:
rsrc
.
Name
})
return
joinDN
(
rdn
,
getUserDN
(
user
,
h
.
baseDN
)),
nil
}
...
...
@@ -347,7 +346,7 @@ func (h *webdavResourceHandler) MakeDN(user *as.User, rsrc *as.Resource) (string
if
user
==
nil
{
return
""
,
errors
.
New
(
"DAV resource requires an owner"
)
}
rdn
:=
replaceVars
(
"ftpname=${resource}"
,
map
[
string
]
string
{
"resource"
:
rsrc
.
Name
})
rdn
:=
replaceVars
(
"ftpname=${resource}"
,
templateVars
{
"resource"
:
rsrc
.
Name
})
return
joinDN
(
rdn
,
getUserDN
(
user
,
h
.
baseDN
)),
nil
}
...
...
@@ -402,7 +401,7 @@ type databaseResourceHandler struct {
}
func
(
h
*
databaseResourceHandler
)
MakeDN
(
user
*
as
.
User
,
rsrc
*
as
.
Resource
)
(
string
,
error
)
{
rdn
:=
replaceVars
(
"dbname=${resource}"
,
map
[
string
]
string
{
"resource"
:
rsrc
.
Name
})
rdn
:=
replaceVars
(
"dbname=${resource}"
,
templateVars
{
"resource"
:
rsrc
.
Name
})
return
joinDN
(
rdn
,
string
(
rsrc
.
ParentID
)),
nil
}
...
...
backend/util.go
View file @
7879718b
...
...
@@ -17,7 +17,13 @@ type queryTemplate struct {
Attrs
[]
string
}
func
(
q
*
queryTemplate
)
query
(
vars
map
[
string
]
string
)
*
ldap
.
SearchRequest
{
// A map of variables to be replaced in a LDAP filter string.
type
templateVars
map
[
string
]
interface
{}
// A rawVariable is a string that won't be escaped.
type
rawVariable
string
func
(
q
*
queryTemplate
)
query
(
vars
templateVars
)
*
ldap
.
SearchRequest
{
filter
:=
q
.
Filter
if
filter
==
""
{
filter
=
"(objectClass=*)"
...
...
@@ -36,9 +42,20 @@ func (q *queryTemplate) query(vars map[string]string) *ldap.SearchRequest {
)
}
func
replaceVars
(
s
string
,
vars
map
[
string
]
string
)
string
{
func
replaceVars
(
s
string
,
vars
templateVars
)
string
{
return
os
.
Expand
(
s
,
func
(
k
string
)
string
{
return
ldap
.
EscapeFilter
(
vars
[
k
])
i
,
ok
:=
vars
[
k
]
if
!
ok
{
return
""
}
switch
v
:=
i
.
(
type
)
{
case
rawVariable
:
return
string
(
v
)
case
string
:
return
ldap
.
EscapeFilter
(
v
)
default
:
return
""
}
})
}
...
...
integrationtest/search_test.go
0 → 100644
View file @
7879718b
package
integrationtest
import
(
"testing"
as
"git.autistici.org/ai3/accountserver"
)
func
TestIntegration_SearchIsAdminPrivileged
(
t
*
testing
.
T
)
{
stop
,
_
,
c
:=
startService
(
t
)
defer
stop
()
var
resp
as
.
SearchUserResponse
if
err
:=
c
.
request
(
"/api/user/search"
,
&
as
.
SearchUserRequest
{
AdminRequestBase
:
as
.
AdminRequestBase
{
RequestBase
:
as
.
RequestBase
{
SSO
:
c
.
ssoTicket
(
"uno@investici.org"
),
},
},
Pattern
:
"uno@investici.org"
,
},
&
resp
);
err
==
nil
{
t
.
Fatal
(
"no error for user-privileged SearchUser!"
)
}
}
func
TestIntegration_SearchUser
(
t
*
testing
.
T
)
{
stop
,
_
,
c
:=
startService
(
t
)
defer
stop
()
var
resp
as
.
SearchUserResponse
if
err
:=
c
.
request
(
"/api/user/search"
,
&
as
.
SearchUserRequest
{
AdminRequestBase
:
as
.
AdminRequestBase
{
RequestBase
:
as
.
RequestBase
{
SSO
:
c
.
ssoTicket
(
testAdminUser
,
testAdminGroup
),
},
},
Pattern
:
"uno@investici.org"
,
},
&
resp
);
err
!=
nil
{
t
.
Fatal
(
err
)
}
if
len
(
resp
.
Usernames
)
!=
1
{
t
.
Fatalf
(
"expected 1 result, got %d"
,
len
(
resp
.
Usernames
))
}
if
resp
.
Usernames
[
0
]
!=
"uno@investici.org"
{
t
.
Fatalf
(
"got bad username '%s', expected uno@investici.org"
,
resp
.
Usernames
[
0
])
}
}
func
TestIntegration_SearchResource
(
t
*
testing
.
T
)
{
stop
,
_
,
c
:=
startService
(
t
)
defer
stop
()
var
resp
as
.
SearchResourceResponse
if
err
:=
c
.
request
(
"/api/resource/search"
,
&
as
.
SearchResourceRequest
{
AdminRequestBase
:
as
.
AdminRequestBase
{
RequestBase
:
as
.
RequestBase
{
SSO
:
c
.
ssoTicket
(
testAdminUser
,
testAdminGroup
),
},
},
Pattern
:
"due*"
,
},
&
resp
);
err
!=
nil
{
t
.
Fatal
(
err
)
}
if
len
(
resp
.
Results
)
!=
4
{
t
.
Fatalf
(
"expected 4 results, got %d"
,
len
(
resp
.
Results
))
}
}
server/server.go
View file @
7879718b
...
...
@@ -48,6 +48,7 @@ func New(service *as.AccountService, backend as.Backend) *APIServer {
}
s
.
Register
(
"/api/user/get"
,
&
as
.
GetUserRequest
{})
s
.
Register
(
"/api/user/search"
,
&
as
.
SearchUserRequest
{})
s
.
Register
(
"/api/user/create"
,
&
as
.
CreateUserRequest
{})
s
.
Register
(
"/api/user/update"
,
&
as
.
UpdateUserRequest
{})
s
.
Register
(
"/api/user/change_password"
,
&
as
.
ChangeUserPasswordRequest
{})
...
...
@@ -56,6 +57,8 @@ func New(service *as.AccountService, backend as.Backend) *APIServer {
s
.
Register
(
"/api/user/disable_otp"
,
&
as
.
DisableOTPRequest
{})
s
.
Register
(
"/api/user/create_app_specific_password"
,
&
as
.
CreateApplicationSpecificPasswordRequest
{})
s
.
Register
(
"/api/user/delete_app_specific_password"
,
&
as
.
DeleteApplicationSpecificPasswordRequest
{})
s
.
Register
(
"/api/resource/get"
,
&
as
.
GetResourceRequest
{})
s
.
Register
(
"/api/resource/search"
,
&
as
.
SearchResourceRequest
{})
s
.
Register
(
"/api/resource/enable"
,
&
as
.
EnableResourceRequest
{})
s
.
Register
(
"/api/resource/disable"
,
&
as
.
DisableResourceRequest
{})
s
.
Register
(
"/api/resource/create"
,
&
as
.
CreateResourcesRequest
{})
...
...
service.go
View file @
7879718b
...
...
@@ -62,6 +62,13 @@ type TX interface {
SetUserTOTPSecret
(
context
.
Context
,
*
User
,
string
)
error
DeleteUserTOTPSecret
(
context
.
Context
,
*
User
)
error
// Lightweight user search (backend-specific pattern).
// Returns list of matching usernames.
SearchUser
(
context
.
Context
,
string
)
([]
string
,
error
)
// Resource search (backend-specific pattern).
SearchResource
(
context
.
Context
,
string
)
([]
*
RawResource
,
error
)
// Resource ACL check (does not necessarily hit the database).
CanAccessResource
(
context
.
Context
,
string
,
*
Resource
)
bool
...
...
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