Commit 7879718b authored by ale's avatar ale

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
...@@ -5,6 +5,57 @@ import ( ...@@ -5,6 +5,57 @@ import (
"fmt" "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 // setResourceStatus sets the status of a single resource (shared
// logic between enable / disable resource methods). // logic between enable / disable resource methods).
func setResourceStatus(rctx *RequestContext, status string) error { func setResourceStatus(rctx *RequestContext, status string) error {
...@@ -100,6 +151,7 @@ type MoveResourceResponse struct { ...@@ -100,6 +151,7 @@ type MoveResourceResponse struct {
// Serve the request. // Serve the request.
func (r *MoveResourceRequest) Serve(rctx *RequestContext) (interface{}, error) { func (r *MoveResourceRequest) Serve(rctx *RequestContext) (interface{}, error) {
resources := []*Resource{rctx.Resource} resources := []*Resource{rctx.Resource}
// If we have an associated user, collect all related // If we have an associated user, collect all related
// resources, as they should all be moved at once. // resources, as they should all be moved at once.
if rctx.User != nil && rctx.Resource.Group != "" { if rctx.User != nil && rctx.Resource.Group != "" {
......
...@@ -52,6 +52,16 @@ func (b *fakeBackend) GetUser(_ context.Context, username string) (*RawUser, err ...@@ -52,6 +52,16 @@ func (b *fakeBackend) GetUser(_ context.Context, username string) (*RawUser, err
}, nil }, 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 { func (b *fakeBackend) UpdateUser(_ context.Context, user *User) error {
b.users[user.Name] = user b.users[user.Name] = user
return nil return nil
......
...@@ -7,7 +7,7 @@ import ( ...@@ -7,7 +7,7 @@ import (
umdb "git.autistici.org/id/usermetadb" umdb "git.autistici.org/id/usermetadb"
) )
// GetUserRequest is the request type for GetUserAction. // GetUserRequest retrieves a specific User.
type GetUserRequest struct { type GetUserRequest struct {
UserRequestBase UserRequestBase
...@@ -25,6 +25,37 @@ func (r *GetUserRequest) Serve(rctx *RequestContext) (interface{}, error) { ...@@ -25,6 +25,37 @@ func (r *GetUserRequest) Serve(rctx *RequestContext) (interface{}, error) {
return &rctx.User.User, nil 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 // ChangeUserPasswordRequest updates a user's password. It will also take
// care of re-encrypting the user encryption key, if present. // care of re-encrypting the user encryption key, if present.
type ChangeUserPasswordRequest struct { type ChangeUserPasswordRequest struct {
......
...@@ -39,6 +39,7 @@ type backend struct { ...@@ -39,6 +39,7 @@ type backend struct {
conn ldapConn conn ldapConn
baseDN string baseDN string
userQuery *queryTemplate userQuery *queryTemplate
searchUserQuery *queryTemplate
userResourceQueries []*queryTemplate userResourceQueries []*queryTemplate
resources *resourceRegistry resources *resourceRegistry
...@@ -88,6 +89,11 @@ func newLDAPBackendWithConn(conn ldapConn, baseDN string) (*backend, error) { ...@@ -88,6 +89,11 @@ func newLDAPBackendWithConn(conn ldapConn, baseDN string) (*backend, error) {
Filter: "(objectClass=*)", Filter: "(objectClass=*)",
Scope: ldap.ScopeBaseObject, Scope: ldap.ScopeBaseObject,
}, },
searchUserQuery: &queryTemplate{
Base: joinDN("ou=People", baseDN),
Filter: "(uid=${pattern}*)",
Scope: ldap.ScopeSingleLevel,
},
userResourceQueries: []*queryTemplate{ userResourceQueries: []*queryTemplate{
// Find all resources that are children of the main uid object. // Find all resources that are children of the main uid object.
&queryTemplate{ &queryTemplate{
...@@ -206,7 +212,7 @@ func (tx *backendTX) UpdateUser(ctx context.Context, user *as.User) error { ...@@ -206,7 +212,7 @@ func (tx *backendTX) UpdateUser(ctx context.Context, user *as.User) error {
// GetUser returns a user. // GetUser returns a user.
func (tx *backendTX) GetUser(ctx context.Context, username string) (*as.RawUser, error) { func (tx *backendTX) GetUser(ctx context.Context, username string) (*as.RawUser, error) {
// First of all, find the main user object, and just that one. // 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)) result, err := tx.search(ctx, tx.backend.userQuery.query(vars))
if err != nil { if err != nil {
if ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) { if ldap.IsErrorWithCode(err, ldap.LDAPResultNoSuchObject) {
...@@ -263,6 +269,24 @@ func (tx *backendTX) GetUser(ctx context.Context, username string) (*as.RawUser, ...@@ -263,6 +269,24 @@ func (tx *backendTX) GetUser(ctx context.Context, username string) (*as.RawUser,
return user, nil 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) { func (tx *backendTX) SetUserPassword(ctx context.Context, user *as.User, encryptedPassword string) (err error) {
dn := tx.getUserDN(user) dn := tx.getUserDN(user)
tx.setAttr(dn, passwordLDAPAttr, encryptedPassword) tx.setAttr(dn, passwordLDAPAttr, encryptedPassword)
...@@ -373,7 +397,7 @@ func (tx *backendTX) hasResource(ctx context.Context, resourceType, resourceName ...@@ -373,7 +397,7 @@ func (tx *backendTX) hasResource(ctx context.Context, resourceType, resourceName
// Make a quick LDAP search that only fetches the DN attribute. // Make a quick LDAP search that only fetches the DN attribute.
tpl.Attrs = []string{"dn"} tpl.Attrs = []string{"dn"}
result, err := tx.search(ctx, tpl.query(map[string]string{ result, err := tx.search(ctx, tpl.query(templateVars{
"resource": resourceName, "resource": resourceName,
"type": resourceType, "type": resourceType,
})) }))
...@@ -400,6 +424,53 @@ func (tx *backendTX) HasAnyResource(ctx context.Context, resourceIDs []as.FindRe ...@@ -400,6 +424,53 @@ func (tx *backendTX) HasAnyResource(ctx context.Context, resourceIDs []as.FindRe
return false, nil 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. // 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) { 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 // From the resource ID we can obtain the DN, and fetch it
......
...@@ -163,6 +163,22 @@ func TestModel_GetUser_Resources(t *testing.T) { ...@@ -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) { func TestModel_SetResourceStatus(t *testing.T) {
stop := ldaptest.StartServer(t, &ldaptest.Config{ stop := ldaptest.StartServer(t, &ldaptest.Config{
Dir: "../ldaptest", Dir: "../ldaptest",
...@@ -236,6 +252,25 @@ func TestModel_HasAnyResource(t *testing.T) { ...@@ -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) { func TestModel_SetUserPassword(t *testing.T) {
stop, b, user := startServerAndGetUser(t) stop, b, user := startServerAndGetUser(t)
defer stop() defer stop()
......
...@@ -25,6 +25,7 @@ type resourceHandler interface { ...@@ -25,6 +25,7 @@ type resourceHandler interface {
// interface to a resourceHandler, with a few exceptions. // interface to a resourceHandler, with a few exceptions.
type resourceRegistry struct { type resourceRegistry struct {
handlers map[string]resourceHandler handlers map[string]resourceHandler
types []string
} }
func newResourceRegistry() *resourceRegistry { func newResourceRegistry() *resourceRegistry {
...@@ -34,10 +35,8 @@ func newResourceRegistry() *resourceRegistry { ...@@ -34,10 +35,8 @@ func newResourceRegistry() *resourceRegistry {
} }
func (reg *resourceRegistry) register(rtype string, h resourceHandler) { func (reg *resourceRegistry) register(rtype string, h resourceHandler) {
if reg.handlers == nil {
reg.handlers = make(map[string]resourceHandler)
}
reg.handlers[rtype] = h reg.handlers[rtype] = h
reg.types = append(reg.types, rtype)
} }
func (reg *resourceRegistry) dispatch(rsrcType string, f func(resourceHandler) error) error { 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, ...@@ -124,7 +123,7 @@ func (h *emailResourceHandler) MakeDN(user *as.User, rsrc *as.Resource) (string,
if user == nil { if user == nil {
return "", errors.New("email resource requires an owner") 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 return joinDN(rdn, getUserDN(user, h.baseDN)), nil
} }
...@@ -173,7 +172,7 @@ type mailingListResourceHandler struct { ...@@ -173,7 +172,7 @@ type mailingListResourceHandler struct {
} }
func (h *mailingListResourceHandler) MakeDN(_ *as.User, rsrc *as.Resource) (string, error) { 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 return joinDN(rdn, "ou=Lists", h.baseDN), nil
} }
...@@ -225,7 +224,7 @@ func (h *websiteResourceHandler) MakeDN(user *as.User, rsrc *as.Resource) (strin ...@@ -225,7 +224,7 @@ func (h *websiteResourceHandler) MakeDN(user *as.User, rsrc *as.Resource) (strin
if user == nil { if user == nil {
return "", errors.New("website resource requires an owner") 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 return joinDN(rdn, getUserDN(user, h.baseDN)), nil
} }
...@@ -289,7 +288,7 @@ func (h *domainResourceHandler) MakeDN(user *as.User, rsrc *as.Resource) (string ...@@ -289,7 +288,7 @@ func (h *domainResourceHandler) MakeDN(user *as.User, rsrc *as.Resource) (string
if user == nil { if user == nil {
return "", errors.New("domain resource requires an owner") 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 return joinDN(rdn, getUserDN(user, h.baseDN)), nil
} }
...@@ -347,7 +346,7 @@ func (h *webdavResourceHandler) MakeDN(user *as.User, rsrc *as.Resource) (string ...@@ -347,7 +346,7 @@ func (h *webdavResourceHandler) MakeDN(user *as.User, rsrc *as.Resource) (string
if user == nil { if user == nil {
return "", errors.New("DAV resource requires an owner") 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 return joinDN(rdn, getUserDN(user, h.baseDN)), nil
} }
...@@ -402,7 +401,7 @@ type databaseResourceHandler struct { ...@@ -402,7 +401,7 @@ type databaseResourceHandler struct {
} }
func (h *databaseResourceHandler) MakeDN(user *as.User, rsrc *as.Resource) (string, error) { 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 return joinDN(rdn, string(rsrc.ParentID)), nil
} }
......
...@@ -17,7 +17,13 @@ type queryTemplate struct { ...@@ -17,7 +17,13 @@ type queryTemplate struct {
Attrs []string 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 filter := q.Filter
if filter == "" { if filter == "" {
filter = "(objectClass=*)" filter = "(objectClass=*)"
...@@ -36,9 +42,20 @@ func (q *queryTemplate) query(vars map[string]string) *ldap.SearchRequest { ...@@ -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 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 ""
}
}) })
} }
......
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))
}
}
...@@ -48,6 +48,7 @@ func New(service *as.AccountService, backend as.Backend) *APIServer { ...@@ -48,6 +48,7 @@ func New(service *as.AccountService, backend as.Backend) *APIServer {
} }
s.Register("/api/user/get", &as.GetUserRequest{}) 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/create", &as.CreateUserRequest{})
s.Register("/api/user/update", &as.UpdateUserRequest{}) s.Register("/api/user/update", &as.UpdateUserRequest{})
s.Register("/api/user/change_password", &as.ChangeUserPasswordRequest{}) s.Register("/api/user/change_password", &as.ChangeUserPasswordRequest{})
...@@ -56,6 +57,8 @@ func New(service *as.AccountService, backend as.Backend) *APIServer { ...@@ -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/disable_otp", &as.DisableOTPRequest{})
s.Register("/api/user/create_app_specific_password", &as.CreateApplicationSpecificPasswordRequest{}) s.Register("/api/user/create_app_specific_password", &as.CreateApplicationSpecificPasswordRequest{})
s.Register("/api/user/delete_app_specific_password", &as.DeleteApplicationSpecificPasswordRequest{}) 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/enable", &as.EnableResourceRequest{})
s.Register("/api/resource/disable", &as.DisableResourceRequest{}) s.Register("/api/resource/disable", &as.DisableResourceRequest{})
s.Register("/api/resource/create", &as.CreateResourcesRequest{}) s.Register("/api/resource/create", &as.CreateResourcesRequest{})
......
...@@ -62,6 +62,13 @@ type TX interface { ...@@ -62,6 +62,13 @@ type TX interface {
SetUserTOTPSecret(context.Context, *User, string) error SetUserTOTPSecret(context.Context, *User, string) error
DeleteUserTOTPSecret(context.Context, *User) 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). // Resource ACL check (does not necessarily hit the database).
CanAccessResource(context.Context, string, *Resource) bool CanAccessResource(context.Context, string, *Resource) bool
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment