Commit 6f6114ec authored by ale's avatar ale

Add a SearchUser admin method to the API

parent a3f4990f
......@@ -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
......
......@@ -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 {
......
......@@ -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{
......@@ -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 := map[string]string{"pattern": 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)
......
......@@ -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",
......
......@@ -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{})
......
......@@ -62,6 +62,10 @@ 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 ACL check (does not necessarily hit the database).
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