Files
opencloud/ocs/pkg/service/v0/groups.go
Ralf Haferkamp f7d290c131 Implement CS3 stub for /users/{userid}/groups
Up to now when using the CS3 backend (e.g. to use an external LDAP
server) queries to /users/{userid}/groups just errored out. This add a
simple stub to just return and empty group list for now.

This allows using and external LDAP server without having to fiddle with
the proxy configuration to redirect to the reva ocs implementation.
(Which also is just returning an empty group list currently)
2022-02-02 17:00:31 +01:00

456 lines
15 KiB
Go

package svc
import (
"context"
"fmt"
"io/ioutil"
"net/http"
"net/url"
"regexp"
"strconv"
accountsmsg "github.com/owncloud/ocis/protogen/gen/ocis/messages/accounts/v0"
accountssvc "github.com/owncloud/ocis/protogen/gen/ocis/services/accounts/v0"
revactx "github.com/cs3org/reva/pkg/ctx"
"github.com/go-chi/chi/v5"
"github.com/owncloud/ocis/ocs/pkg/service/v0/data"
"github.com/owncloud/ocis/ocs/pkg/service/v0/response"
ocstracing "github.com/owncloud/ocis/ocs/pkg/tracing"
merrors "go-micro.dev/v4/errors"
"go.opentelemetry.io/otel/attribute"
)
// ListUserGroups lists a users groups
func (o Ocs) ListUserGroups(w http.ResponseWriter, r *http.Request) {
userid := chi.URLParam(r, "userid")
userid, err := url.PathUnescape(userid)
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
var account *accountsmsg.Account
if o.config.AccountBackend == "cs3" {
o.mustRender(w, r, response.DataRender(&data.Groups{}))
return
}
// short circuit if there is a user already in the context
if u, ok := revactx.ContextGetUser(r.Context()); ok {
// we are not sure whether the current user in the context is the admin or the authenticated user.
if u.Username == userid {
// the OCS API is a REST API and it uses the username to look for groups. If the id from the user in the context
// differs from that of the url we can assume we are an admin because we are past the selfOrAdmin middleware.
_, span := ocstracing.TraceProvider.
Tracer("ocs").
Start(r.Context(), "ListUserGroups")
defer span.End()
span.SetAttributes(attribute.StringSlice("groups", u.Groups))
if len(u.Groups) > 0 {
o.mustRender(w, r, response.DataRender(&data.Groups{Groups: u.Groups}))
return
}
}
}
if isValidUUID(userid) {
account, err = o.getAccountService().GetAccount(r.Context(), &accountssvc.GetAccountRequest{
Id: userid,
})
} else {
// despite the confusion, if we make it here we got ourselves a username
account, err = o.fetchAccountByUsername(r.Context(), userid)
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageUserNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("userid", userid).Msg("could not get list of user groups")
return
}
}
groups := make([]string, 0, len(account.MemberOf))
for i := range account.MemberOf {
if account.MemberOf[i].OnPremisesSamAccountName == "" {
o.logger.Warn().Str("groupid", account.MemberOf[i].Id).Msg("group on_premises_sam_account_name is empty, trying to lookup by id")
// we can try to look up the name
group, err := o.getGroupsService().GetGroup(r.Context(), &accountssvc.GetGroupRequest{
Id: account.MemberOf[i].Id,
})
if err != nil {
o.logger.Error().Err(err).Str("groupid", account.MemberOf[i].Id).Msg("could not get group")
continue
}
if group.OnPremisesSamAccountName == "" {
o.logger.Error().Err(err).Str("groupid", account.MemberOf[i].Id).Msg("group on_premises_sam_account_name is empty")
continue
}
groups = append(groups, group.OnPremisesSamAccountName)
} else {
groups = append(groups, account.MemberOf[i].OnPremisesSamAccountName)
}
}
o.logger.Error().Err(err).Int("count", len(groups)).Str("userid", account.Id).Msg("listing groups for user")
_, span := ocstracing.TraceProvider.
Tracer("ocs").
Start(r.Context(), "ListUserGroups")
defer span.End()
span.SetAttributes(attribute.StringSlice("groups", groups))
o.mustRender(w, r, response.DataRender(&data.Groups{Groups: groups}))
}
// AddToGroup adds a user to a group
func (o Ocs) AddToGroup(w http.ResponseWriter, r *http.Request) {
groupid := r.PostFormValue("groupid")
userid := chi.URLParam(r, "userid")
userid, err := url.PathUnescape(userid)
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
if groupid == "" {
o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, "empty group assignment: unspecified group"))
return
}
account, err := o.fetchAccountByUsername(r.Context(), userid)
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageUserNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
return
}
// ocs only knows about names so we have to look up the internal id
group, err := o.fetchGroupByName(r.Context(), groupid)
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageGroupNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
return
}
_, err = o.getGroupsService().AddMember(r.Context(), &accountssvc.AddMemberRequest{
AccountId: account.Id,
GroupId: group.Id,
})
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageGroupNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("userid", account.Id).Str("groupid", group.Id).Msg("could not add user to group")
return
}
o.logger.Debug().Str("userid", account.Id).Str("groupid", group.Id).Msg("added user to group")
o.mustRender(w, r, response.DataRender(struct{}{}))
}
// RemoveFromGroup removes a user from a group
func (o Ocs) RemoveFromGroup(w http.ResponseWriter, r *http.Request) {
userid := chi.URLParam(r, "userid")
userid, err := url.PathUnescape(userid)
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
// Really? a DELETE with form encoded body?!?
// but it is not encoded as mime, so we cannot just call r.ParseForm()
// read it manually
body, err := ioutil.ReadAll(r.Body)
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, err.Error()))
return
}
if err = r.Body.Close(); err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
return
}
values, err := url.ParseQuery(string(body))
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, err.Error()))
return
}
groupid := values.Get("groupid")
if groupid == "" {
o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, "no group id"))
return
}
var account *accountsmsg.Account
if isValidUUID(userid) {
account, _ = o.getAccountService().GetAccount(r.Context(), &accountssvc.GetAccountRequest{
Id: userid,
})
} else {
// despite the confusion, if we make it here we got ourselves a username
account, err = o.fetchAccountByUsername(r.Context(), userid)
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, data.MessageUserNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("userid", userid).Msg("could not get list of user groups")
return
}
}
// ocs only knows about names so we have to look up the internal id
group, err := o.fetchGroupByName(r.Context(), groupid)
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageGroupNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
return
}
_, err = o.getGroupsService().RemoveMember(r.Context(), &accountssvc.RemoveMemberRequest{
AccountId: account.Id,
GroupId: group.Id,
})
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageGroupNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("userid", account.Id).Str("groupid", group.Id).Msg("could not remove user from group")
return
}
o.logger.Debug().Str("userid", account.Id).Str("groupid", group.Id).Msg("removed user from group")
o.mustRender(w, r, response.DataRender(struct{}{}))
}
// ListGroups lists all groups
func (o Ocs) ListGroups(w http.ResponseWriter, r *http.Request) {
search := r.URL.Query().Get("search")
query := ""
if search != "" {
query = fmt.Sprintf("id eq '%s' or on_premises_sam_account_name eq '%s'", escapeValue(search), escapeValue(search))
}
res, err := o.getGroupsService().ListGroups(r.Context(), &accountssvc.ListGroupsRequest{
Query: query,
})
if err != nil {
o.logger.Err(err).Msg("could not list users")
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, "could not list users"))
return
}
groups := make([]string, 0, len(res.Groups))
for i := range res.Groups {
groups = append(groups, res.Groups[i].OnPremisesSamAccountName)
}
_, span := ocstracing.TraceProvider.
Tracer("ocs").
Start(r.Context(), "ListGroups")
defer span.End()
span.SetAttributes(attribute.StringSlice("groups", groups))
o.mustRender(w, r, response.DataRender(&data.Groups{Groups: groups}))
}
// AddGroup adds a group
// oC10 implementation: https://github.com/owncloud/core/blob/762780a23c9eadda4fb5fa8db99eba66a5100b6e/apps/provisioning_api/lib/Groups.php#L126-L154
func (o Ocs) AddGroup(w http.ResponseWriter, r *http.Request) {
groupid := r.PostFormValue("groupid")
displayname := r.PostFormValue("displayname")
gid := r.PostFormValue("gidnumber")
if displayname == "" && groupid == "" {
code := data.MetaFailure.StatusCode // v1
if response.APIVersion(r.Context()) == "2" {
code = data.MetaBadRequest.StatusCode
}
o.mustRender(w, r, response.ErrRender(code, "No groupid or display name provided"))
return
}
if displayname == "" {
// oC10 OCS does not know about a group displayname
// therefore we fall back to the oC10 parameter groupid (which is the groupname in the oC10 world)
displayname = groupid
}
var gidNumber int64
var err error
if gid != "" {
gidNumber, err = strconv.ParseInt(gid, 10, 64)
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, "Cannot use the gidnumber provided"))
o.logger.Error().Err(err).Str("gid", gid).Str("groupid", groupid).Msg("Cannot use the gidnumber provided")
return
}
}
newGroup := &accountsmsg.Group{
Id: groupid,
DisplayName: displayname,
OnPremisesSamAccountName: groupid,
GidNumber: gidNumber,
}
group, err := o.getGroupsService().CreateGroup(r.Context(), &accountssvc.CreateGroupRequest{
Group: newGroup,
})
if err != nil {
merr := merrors.FromError(err)
switch merr.Code {
case http.StatusBadRequest:
o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, merr.Detail))
case http.StatusConflict:
if response.APIVersion(r.Context()) == "2" {
// it seems the application framework sets the ocs status code to the httpstatus code, which affects the provisioning api
// see https://github.com/owncloud/core/blob/b9ff4c93e051c94adfb301545098ae627e52ef76/lib/public/AppFramework/OCSController.php#L142-L150
o.mustRender(w, r, response.ErrRender(data.MetaBadRequest.StatusCode, merr.Detail))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaInvalidInput.StatusCode, merr.Detail))
}
default:
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("groupid", groupid).Msg("could not add group")
// TODO check error if group already existed
return
}
o.logger.Debug().Interface("group", group).Msg("added group")
o.mustRender(w, r, response.DataRender(struct{}{}))
}
// DeleteGroup deletes a group
func (o Ocs) DeleteGroup(w http.ResponseWriter, r *http.Request) {
groupid := chi.URLParam(r, "groupid")
groupid, err := url.PathUnescape(groupid)
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
// ocs only knows about names so we have to look up the internal id
group, err := o.fetchGroupByName(r.Context(), groupid)
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageGroupNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
return
}
_, err = o.getGroupsService().DeleteGroup(r.Context(), &accountssvc.DeleteGroupRequest{
Id: group.Id,
})
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageGroupNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("groupid", group.Id).Msg("could not remove group")
return
}
o.logger.Debug().Str("groupid", group.Id).Msg("removed group")
o.mustRender(w, r, response.DataRender(struct{}{}))
}
// GetGroupMembers lists all members of a group
func (o Ocs) GetGroupMembers(w http.ResponseWriter, r *http.Request) {
groupid := chi.URLParam(r, "groupid")
groupid, err := url.PathUnescape(groupid)
if err != nil {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
// ocs only knows about names so we have to look up the internal id
group, err := o.fetchGroupByName(r.Context(), groupid)
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageGroupNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
return
}
res, err := o.getGroupsService().ListMembers(r.Context(), &accountssvc.ListMembersRequest{Id: group.Id})
if err != nil {
merr := merrors.FromError(err)
if merr.Code == http.StatusNotFound {
o.mustRender(w, r, response.ErrRender(data.MetaNotFound.StatusCode, data.MessageGroupNotFound))
} else {
o.mustRender(w, r, response.ErrRender(data.MetaServerError.StatusCode, err.Error()))
}
o.logger.Error().Err(err).Str("groupid", group.Id).Msg("could not get list of members")
return
}
members := make([]string, 0, len(res.Members))
for i := range res.Members {
members = append(members, res.Members[i].OnPremisesSamAccountName)
}
o.logger.Error().Err(err).Int("count", len(members)).Str("groupid", groupid).Msg("listing group members")
o.mustRender(w, r, response.DataRender(&data.Users{Users: members}))
}
func isValidUUID(uuid string) bool {
r := regexp.MustCompile("^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-4[a-fA-F0-9]{3}-[8|9|aA|bB][a-fA-F0-9]{3}-[a-fA-F0-9]{12}$")
return r.MatchString(uuid)
}
func (o Ocs) fetchGroupByName(ctx context.Context, name string) (*accountsmsg.Group, error) {
var res *accountssvc.ListGroupsResponse
res, err := o.getGroupsService().ListGroups(ctx, &accountssvc.ListGroupsRequest{
Query: fmt.Sprintf("on_premises_sam_account_name eq '%v'", escapeValue(name)),
})
if err != nil {
return nil, err
}
if res != nil && len(res.Groups) == 1 {
return res.Groups[0], nil
}
return nil, merrors.NotFound("", data.MessageGroupNotFound)
}