Commit dd4cfa28 authored by ale's avatar ale

Add a SearchResource API

To support pattern searches, make the LDAP query templates understand
both "admin-provided input" and "user-provided input", so that
wildcards will only be escaped in the latter case.
parent da224549
......@@ -16,6 +16,7 @@ type GetResourceResponse struct {
Owner string `json:"owner"`
}
// Serve the request.
func (r *GetResourceRequest) Serve(rctx *RequestContext) (interface{}, error) {
resp := GetResourceResponse{
Resource: rctx.Resource,
......@@ -26,6 +27,35 @@ func (r *GetResourceRequest) Serve(rctx *RequestContext) (interface{}, error) {
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 {
......
......@@ -212,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) {
......@@ -271,7 +271,7 @@ func (tx *backendTX) GetUser(ctx context.Context, username string) (*as.RawUser,
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}
vars := templateVars{"pattern": rawVariable(pattern)}
result, err := tx.search(ctx, tx.backend.searchUserQuery.query(vars))
if err != nil {
return nil, err
......@@ -397,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,
}))
......@@ -424,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
......
......@@ -252,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()
......
......@@ -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
}
......
......@@ -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 ""
}
})
}
......
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))
}
}
......@@ -58,6 +58,7 @@ func New(service *as.AccountService, backend as.Backend) *APIServer {
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{})
......
......@@ -66,6 +66,9 @@ type TX interface {
// 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
......
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