Files
opencloud/pkg/service/v0/groups.go
T
Jörn Friedrich Dreyer 0d4e6a4a16 initial group api implementation
Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
2020-07-10 15:51:13 +02:00

322 lines
9.7 KiB
Go

package service
import (
"context"
"encoding/json"
"errors"
"io/ioutil"
"os"
"path/filepath"
"github.com/CiscoM31/godata"
"github.com/blevesearch/bleve"
"github.com/blevesearch/bleve/search/query"
"github.com/gofrs/uuid"
"github.com/golang/protobuf/ptypes/empty"
merrors "github.com/micro/go-micro/v2/errors"
"github.com/owncloud/ocis-accounts/pkg/proto/v0"
"github.com/owncloud/ocis-accounts/pkg/provider"
)
func (s Service) indexGroups(path string) (err error) {
var f *os.File
if f, err = os.Open(path); err != nil {
s.log.Error().Err(err).Str("dir", path).Msg("could not open groups folder")
return
}
list, err := f.Readdir(-1)
f.Close()
if err != nil {
s.log.Error().Err(err).Str("dir", path).Msg("could not list groups folder")
return
}
for _, file := range list {
g := &proto.Group{}
if err = s.loadGroup(file.Name(), g); err != nil {
s.log.Error().Err(err).Str("group", file.Name()).Msg("could not load group")
continue
}
s.log.Debug().Interface("group", g).Msg("found group")
if err = s.index.Index(g.Id, g); err != nil {
s.log.Error().Err(err).Interface("group", g).Msg("could not index group")
continue
}
}
return
}
func (s Service) loadGroup(id string, g *proto.Group) (err error) {
path := filepath.Join(s.Config.Server.AccountsDataPath, "groups", id)
var data []byte
if data, err = ioutil.ReadFile(path); err != nil {
return merrors.NotFound(s.id, "could not read group: %v", err.Error())
}
if err = json.Unmarshal(data, g); err != nil {
return merrors.InternalServerError(s.id, "could not unmarshal group: %v", err.Error())
}
return
}
func (s Service) writeGroup(g *proto.Group) (err error) {
// leave only the member id
s.inflateMembers(g)
var bytes []byte
if bytes, err = json.Marshal(g); err != nil {
return merrors.InternalServerError(s.id, "could not marshal group: %v", err.Error())
}
path := filepath.Join(s.Config.Server.AccountsDataPath, "groups", g.Id)
if err = ioutil.WriteFile(path, bytes, 0600); err != nil {
return merrors.InternalServerError(s.id, "could not write group: %v", err.Error())
}
return
}
func (s Service) expandMembers(g *proto.Group) {
if g == nil {
return
}
expanded := []*proto.Account{}
for i := range g.Members {
// TODO resolve by name, when a create or update is issued they may not have an id? fall back to searching the group id in the index?
a := &proto.Account{}
if err := s.loadAccount(g.Members[i].Id, a); err == nil {
expanded = append(expanded, a)
} else {
// log errors but continue execution for now
s.log.Error().Err(err).Str("id", g.Members[i].Id).Msg("could not load account")
}
}
g.Members = expanded
}
// inflateGroups replaces the groups of a user with an instance that only contains the id
func (s Service) inflateMembers(g *proto.Group) {
if g == nil {
return
}
inflated := []*proto.Account{}
for i := range g.Members {
if g.Members[i].Id != "" {
inflated = append(inflated, &proto.Account{Id: g.Members[i].Id})
} else {
// TODO fetch and use an id when group only has a name but no id
s.log.Error().Str("id", g.Id).Interface("account", g.Members[i]).Msg("resolving members by name is not implemented yet")
}
}
g.Members = inflated
}
// ListGroups implements the GroupsServiceHandler interface
func (s Service) ListGroups(c context.Context, in *proto.ListGroupsRequest, out *proto.ListGroupsResponse) (err error) {
var query query.Query
if in.Query != "" {
// parse the query like an odata filter
var q *godata.GoDataFilterQuery
if q, err = godata.ParseFilterString(in.Query); err != nil {
s.log.Error().Err(err).Msg("could not parse query")
return merrors.InternalServerError(s.id, "could not parse query: %v", err.Error())
}
// convert to bleve query
query, err = provider.BuildBleveQuery(q)
if err != nil {
s.log.Error().Err(err).Msg("could not build bleve query")
return merrors.InternalServerError(s.id, "could not build bleve query: %v", err.Error())
}
} else {
query = bleve.NewMatchAllQuery()
}
s.log.Debug().Interface("query", query).Msg("using query")
searchRequest := bleve.NewSearchRequest(query)
var searchResult *bleve.SearchResult
searchResult, err = s.index.Search(searchRequest)
if err != nil {
s.log.Error().Err(err).Msg("could not execute bleve search")
return merrors.InternalServerError(s.id, "could not execute bleve search: %v", err.Error())
}
s.log.Debug().Interface("result", searchResult).Msg("result")
out.Groups = make([]*proto.Group, 0)
for _, hit := range searchResult.Hits {
g := &proto.Group{}
if err = s.loadGroup(hit.ID, g); err != nil {
s.log.Error().Err(err).Str("group", hit.ID).Msg("could not load group, skipping")
continue
}
s.log.Debug().Interface("group", g).Msg("found group")
// TODO add accounts if requested
// if in.FieldMask ...
s.expandMembers(g)
out.Groups = append(out.Groups, g)
}
return
}
// GetGroup implements the GroupsServiceHandler interface
func (s Service) GetGroup(c context.Context, in *proto.GetGroupRequest, out *proto.Group) (err error) {
var id string
if id, err = cleanupID(in.Id); err != nil {
return merrors.InternalServerError(s.id, "could not clean up group id: %v", err.Error())
}
if err = s.loadGroup(id, out); err != nil {
s.log.Error().Err(err).Str("id", id).Msg("could not load group")
return
}
s.log.Debug().Interface("group", out).Msg("found group")
// TODO only add accounts if requested
// if in.FieldMask ...
s.expandMembers(out)
return
}
// CreateGroup implements the GroupsServiceHandler interface
func (s Service) CreateGroup(c context.Context, in *proto.CreateGroupRequest, out *proto.Group) (err error) {
var id string
if in.Group == nil {
return merrors.BadRequest(s.id, "account missing")
}
if in.Group.Id == "" {
in.Group.Id = uuid.Must(uuid.NewV4()).String()
}
if id, err = cleanupID(in.Group.Id); err != nil {
return merrors.InternalServerError(s.id, "could not clean up account id: %v", err.Error())
}
path := filepath.Join(s.Config.Server.AccountsDataPath, "groups", id)
// extract member id
s.inflateMembers(in.Group)
if err = s.writeGroup(in.Group); err != nil {
s.log.Error().Err(err).Interface("group", in.Group).Msg("could not persist new group")
return
}
if err = s.index.Index(id, in.Group); err != nil {
s.log.Error().Err(err).Str("id", id).Str("path", path).Interface("group", in.Group).Msg("could not index new group")
return merrors.InternalServerError(s.id, "could not index new group: %v", err.Error())
}
return
}
// UpdateGroup implements the GroupsServiceHandler interface
func (s Service) UpdateGroup(c context.Context, in *proto.UpdateGroupRequest, out *proto.Group) (err error) {
return merrors.InternalServerError(s.id, "not implemented")
}
// DeleteGroup implements the GroupsServiceHandler interface
func (s Service) DeleteGroup(c context.Context, in *proto.DeleteGroupRequest, out *empty.Empty) (err error) {
var id string
if id, err = cleanupID(in.Id); err != nil {
return merrors.InternalServerError(s.id, "could not clean up group id: %v", err.Error())
}
path := filepath.Join(s.Config.Server.AccountsDataPath, "groups", id)
if err = os.Remove(path); err != nil {
s.log.Error().Err(err).Str("id", id).Str("path", path).Msg("could not remove group")
return merrors.InternalServerError(s.id, "could not remove group: %v", err.Error())
}
if err = s.index.Delete(id); err != nil {
s.log.Error().Err(err).Str("id", id).Str("path", path).Msg("could not remove group from index")
return merrors.InternalServerError(s.id, "could not remove group from index: %v", err.Error())
}
// TODO delete memberof relationship in users
return
}
// AddMember implements the GroupsServiceHandler interface
func (s Service) AddMember(c context.Context, in *proto.AddMemberRequest, out *proto.Group) (err error) {
// cleanup ids
var groupID string
if groupID, err = cleanupID(in.GroupId); err != nil {
return merrors.InternalServerError(s.id, "could not clean up group id: %v", err.Error())
}
var accountID string
if accountID, err = cleanupID(in.AccountId); err != nil {
return merrors.InternalServerError(s.id, "could not clean up account id: %v", err.Error())
}
// load structs
a := &proto.Account{}
if err = s.loadAccount(accountID, a); err != nil {
s.log.Error().Err(err).Str("id", accountID).Msg("could not load account")
return
}
g := &proto.Group{}
if err = s.loadGroup(groupID, g); err != nil {
s.log.Error().Err(err).Str("id", groupID).Msg("could not load group")
return
}
// check if we need to add the account to the group
alreadyRelated := false
for i := range g.Members {
if g.Members[i].Id == a.Id {
alreadyRelated = true
}
}
if alreadyRelated == false {
g.Members = append(g.Members, a)
}
// check if we need to add the group to the account
alreadyRelated = false
for i := range a.MemberOf {
if a.MemberOf[i].Id == g.Id {
alreadyRelated = true
}
}
if alreadyRelated == false {
a.MemberOf = append(a.MemberOf, g)
}
if err = s.writeAccount(a); err != nil {
s.log.Error().Err(err).Interface("account", a).Msg("could not persist account")
return
}
if err = s.writeGroup(g); err != nil {
s.log.Error().Err(err).Interface("group", g).Msg("could not persist group")
return
}
// TODO rollback changes when only one of them failed?
// TODO store relation in another file?
// TODO return error if they are already related?
return nil
}
// RemoveMember implements the GroupsServiceHandler interface
func (s Service) RemoveMember(c context.Context, in *proto.RemoveMemberRequest, out *proto.Group) error {
return errors.New("not implemented")
}
// ListMembers implements the GroupsServiceHandler interface
func (s Service) ListMembers(c context.Context, in *proto.ListMembersRequest, out *proto.ListMembersResponse) error {
return errors.New("not implemented")
}