mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-04 03:09:33 -06:00
[full-ci] standalone graph service with LDAP (#5199)
* standalone graph service with LDAP Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * no panic on PATCH and DELETE Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * fix apitoken yaml key Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * update user, fix response codes Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * fix group creation return code Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * remove unknown user property Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * fix create return code checks in graph feature context Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * updating uses 200 OK when returning a body Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * revert user statusCreated change for now Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> * revert return code changes Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de> Signed-off-by: Jörn Friedrich Dreyer <jfd@butonic.de>
This commit is contained in:
committed by
GitHub
parent
4123c7e2d8
commit
d359a7c2cf
@@ -8,4 +8,5 @@ type HTTP struct {
|
||||
Namespace string `yaml:"-"`
|
||||
Root string `yaml:"root" env:"GRAPH_HTTP_ROOT" desc:"Subdirectory that serves as the root for this HTTP service."`
|
||||
TLS shared.HTTPServiceTLS `yaml:"tls"`
|
||||
APIToken string `yaml:"apitoken" env:"GRAPH_HTTP_API_TOKEN" desc:"An optional API bearer token"`
|
||||
}
|
||||
|
||||
@@ -280,6 +280,7 @@ func (i *LDAP) UpdateUser(ctx context.Context, nameOrID string, user libregraph.
|
||||
updateNeeded = true
|
||||
}
|
||||
}
|
||||
// TODO implement account disabled/enabled
|
||||
|
||||
if updateNeeded {
|
||||
if err := i.conn.Modify(&mr); err != nil {
|
||||
|
||||
@@ -4,16 +4,20 @@ import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
stdhttp "net/http"
|
||||
"os"
|
||||
|
||||
"github.com/cs3org/reva/v2/pkg/events/server"
|
||||
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
|
||||
chimiddleware "github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/go-micro/plugins/v4/events/natsjs"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/account"
|
||||
ociscrypto "github.com/owncloud/ocis/v2/ocis-pkg/crypto"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/middleware"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/service/grpc"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/service/http"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/version"
|
||||
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
|
||||
graphMiddleware "github.com/owncloud/ocis/v2/services/graph/pkg/middleware"
|
||||
svc "github.com/owncloud/ocis/v2/services/graph/pkg/service/v0"
|
||||
"github.com/pkg/errors"
|
||||
@@ -82,25 +86,49 @@ func Server(opts ...Option) (http.Service, error) {
|
||||
}
|
||||
}
|
||||
|
||||
handle := svc.NewService(
|
||||
svc.Logger(options.Logger),
|
||||
svc.Config(options.Config),
|
||||
svc.Middleware(
|
||||
middleware.TraceContext,
|
||||
chimiddleware.RequestID,
|
||||
middleware.Version(
|
||||
"graph",
|
||||
version.GetString(),
|
||||
),
|
||||
middleware.Logger(
|
||||
options.Logger,
|
||||
),
|
||||
middlewares := []func(stdhttp.Handler) stdhttp.Handler{
|
||||
middleware.TraceContext,
|
||||
chimiddleware.RequestID,
|
||||
middleware.Version(
|
||||
"graph",
|
||||
version.GetString(),
|
||||
),
|
||||
middleware.Logger(
|
||||
options.Logger,
|
||||
),
|
||||
}
|
||||
// how do we secure the api?
|
||||
var requireAdminMiddleware func(stdhttp.Handler) stdhttp.Handler
|
||||
var roleService svc.RoleService
|
||||
var gatewayClient svc.GatewayClient
|
||||
if options.Config.HTTP.APIToken == "" {
|
||||
middlewares = append(middlewares,
|
||||
graphMiddleware.Auth(
|
||||
account.Logger(options.Logger),
|
||||
account.JWTSecret(options.Config.TokenManager.JWTSecret),
|
||||
),
|
||||
),
|
||||
))
|
||||
roleService = settingssvc.NewRoleService("com.owncloud.api.settings", grpc.DefaultClient())
|
||||
gatewayClient, err = pool.GetGatewayServiceClient(options.Config.Reva.Address, options.Config.Reva.GetRevaOptions()...)
|
||||
if err != nil {
|
||||
return http.Service{}, errors.Wrap(err, "could not initialize gateway client")
|
||||
}
|
||||
} else {
|
||||
middlewares = append(middlewares, middleware.Token(options.Config.HTTP.APIToken))
|
||||
// use a dummy admin middleware for the chi router
|
||||
requireAdminMiddleware = func(next stdhttp.Handler) stdhttp.Handler {
|
||||
return next
|
||||
}
|
||||
// no gatewayclient needed
|
||||
}
|
||||
|
||||
handle := svc.NewService(
|
||||
svc.Logger(options.Logger),
|
||||
svc.Config(options.Config),
|
||||
svc.Middleware(middlewares...),
|
||||
svc.EventsPublisher(publisher),
|
||||
svc.WithRoleService(roleService),
|
||||
svc.WithRequireAdminMiddleware(requireAdminMiddleware),
|
||||
svc.WithGatewayClient(gatewayClient),
|
||||
)
|
||||
|
||||
if handle == nil {
|
||||
|
||||
@@ -92,7 +92,7 @@ func (g Graph) PostGroup(w http.ResponseWriter, r *http.Request) {
|
||||
currentUser := revactx.ContextMustGetUser(r.Context())
|
||||
g.publishEvent(events.GroupCreated{Executant: currentUser.Id, GroupID: *grp.Id})
|
||||
}
|
||||
render.Status(r, http.StatusOK)
|
||||
render.Status(r, http.StatusOK) // FIXME 201 should return 201 created
|
||||
render.JSON(w, r, grp)
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ func (g Graph) PatchGroup(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
return
|
||||
}
|
||||
render.Status(r, http.StatusNoContent)
|
||||
render.Status(r, http.StatusNoContent) // TODO StatusNoContent when prefer=minimal is used, otherwise OK and the resource in the body
|
||||
render.NoContent(w, r)
|
||||
}
|
||||
|
||||
|
||||
@@ -222,7 +222,7 @@ var _ = Describe("Groups", func() {
|
||||
Expect(rr.Code).To(Equal(http.StatusBadRequest))
|
||||
})
|
||||
|
||||
It("disallows user create ids", func() {
|
||||
It("disallows group create ids", func() {
|
||||
newGroup = libregraph.NewGroup()
|
||||
newGroup.SetId("disallowed")
|
||||
newGroup.SetDisplayName("New Group")
|
||||
|
||||
@@ -16,15 +16,16 @@ type Option func(o *Options)
|
||||
|
||||
// Options defines the available options for this package.
|
||||
type Options struct {
|
||||
Logger log.Logger
|
||||
Config *config.Config
|
||||
Middleware []func(http.Handler) http.Handler
|
||||
GatewayClient GatewayClient
|
||||
IdentityBackend identity.Backend
|
||||
RoleService RoleService
|
||||
PermissionService Permissions
|
||||
RoleManager *roles.Manager
|
||||
EventsPublisher events.Publisher
|
||||
Logger log.Logger
|
||||
Config *config.Config
|
||||
Middleware []func(http.Handler) http.Handler
|
||||
RequireAdminMiddleware func(http.Handler) http.Handler
|
||||
GatewayClient GatewayClient
|
||||
IdentityBackend identity.Backend
|
||||
RoleService RoleService
|
||||
PermissionService Permissions
|
||||
RoleManager *roles.Manager
|
||||
EventsPublisher events.Publisher
|
||||
}
|
||||
|
||||
// newOptions initializes the available default options.
|
||||
@@ -59,6 +60,13 @@ func Middleware(val ...func(http.Handler) http.Handler) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithRequireAdminMiddleware provides a function to set the RequireAdminMiddleware option.
|
||||
func WithRequireAdminMiddleware(val func(http.Handler) http.Handler) Option {
|
||||
return func(o *Options) {
|
||||
o.RequireAdminMiddleware = val
|
||||
}
|
||||
}
|
||||
|
||||
// WithGatewayClient provides a function to set the gateway client option.
|
||||
func WithGatewayClient(val GatewayClient) Option {
|
||||
return func(o *Options) {
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/cs3org/reva/v2/pkg/rgrpc/todo/pool"
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/go-chi/chi/v5/middleware"
|
||||
"github.com/jellydator/ttlcache/v2"
|
||||
@@ -69,17 +68,9 @@ func NewService(opts ...Option) Service {
|
||||
logger: &options.Logger,
|
||||
spacePropertiesCache: ttlcache.NewCache(),
|
||||
eventsPublisher: options.EventsPublisher,
|
||||
gatewayClient: options.GatewayClient,
|
||||
}
|
||||
if options.GatewayClient == nil {
|
||||
var err error
|
||||
svc.gatewayClient, err = pool.GetGatewayServiceClient(options.Config.Reva.Address, options.Config.Reva.GetRevaOptions()...)
|
||||
if err != nil {
|
||||
options.Logger.Error().Err(err).Msg("Could not get gateway client")
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
svc.gatewayClient = options.GatewayClient
|
||||
}
|
||||
|
||||
if options.IdentityBackend == nil {
|
||||
switch options.Config.Identity.Backend {
|
||||
case "cs3":
|
||||
@@ -145,12 +136,6 @@ func NewService(opts ...Option) Service {
|
||||
svc.identityBackend = options.IdentityBackend
|
||||
}
|
||||
|
||||
if options.RoleService == nil {
|
||||
svc.roleService = settingssvc.NewRoleService("com.owncloud.api.settings", grpc.DefaultClient())
|
||||
} else {
|
||||
svc.roleService = options.RoleService
|
||||
}
|
||||
|
||||
if options.PermissionService == nil {
|
||||
svc.permissionsService = settingssvc.NewPermissionService("com.owncloud.api.settings", grpc.DefaultClient())
|
||||
} else {
|
||||
@@ -167,12 +152,17 @@ func NewService(opts ...Option) Service {
|
||||
m := roles.NewManager(
|
||||
roles.StoreOptions(storeOptions),
|
||||
roles.Logger(options.Logger),
|
||||
roles.RoleService(svc.roleService),
|
||||
roles.RoleService(options.RoleService),
|
||||
)
|
||||
roleManager = &m
|
||||
}
|
||||
|
||||
requireAdmin := graphm.RequireAdmin(roleManager, options.Logger)
|
||||
var requireAdmin func(http.Handler) http.Handler
|
||||
if options.RequireAdminMiddleware == nil {
|
||||
requireAdmin = graphm.RequireAdmin(roleManager, options.Logger)
|
||||
} else {
|
||||
requireAdmin = options.RequireAdminMiddleware
|
||||
}
|
||||
|
||||
m.Route(options.Config.HTTP.Root, func(r chi.Router) {
|
||||
r.Use(middleware.StripSlashes)
|
||||
|
||||
@@ -158,28 +158,28 @@ func (g Graph) PostUser(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
// All users get the user role by default currently.
|
||||
// to all new users for now, as create Account request does not have any role field
|
||||
if g.roleService == nil {
|
||||
// log as error, admin needs to do something about it
|
||||
logger.Error().Str("id", *u.Id).Msg("could not create user: role service not configured")
|
||||
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "could not assign role to account: roleService not configured")
|
||||
return
|
||||
}
|
||||
if _, err = g.roleService.AssignRoleToUser(r.Context(), &settings.AssignRoleToUserRequest{
|
||||
AccountUuid: *u.Id,
|
||||
RoleId: settingssvc.BundleUUIDRoleUser,
|
||||
}); err != nil {
|
||||
// log as error, admin eventually needs to do something
|
||||
logger.Error().Err(err).Str("id", *u.Id).Str("role", settingssvc.BundleUUIDRoleUser).Msg("could not create user: role assignment failed")
|
||||
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "role assignment failed")
|
||||
return
|
||||
// assign roles if possible
|
||||
if g.roleService != nil {
|
||||
// All users get the user role by default currently.
|
||||
// to all new users for now, as create Account request does not have any role field
|
||||
if _, err = g.roleService.AssignRoleToUser(r.Context(), &settings.AssignRoleToUserRequest{
|
||||
AccountUuid: *u.Id,
|
||||
RoleId: settingssvc.BundleUUIDRoleUser,
|
||||
}); err != nil {
|
||||
// log as error, admin eventually needs to do something
|
||||
logger.Error().Err(err).Str("id", *u.Id).Str("role", settingssvc.BundleUUIDRoleUser).Msg("could not create user: role assignment failed")
|
||||
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "role assignment failed")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
currentUser := revactx.ContextMustGetUser(r.Context())
|
||||
g.publishEvent(events.UserCreated{Executant: currentUser.Id, UserID: *u.Id})
|
||||
e := events.UserCreated{UserID: *u.Id}
|
||||
if currentUser, ok := revactx.ContextGetUser(r.Context()); ok {
|
||||
e.Executant = currentUser.GetId()
|
||||
}
|
||||
g.publishEvent(e)
|
||||
|
||||
render.Status(r, http.StatusOK)
|
||||
render.Status(r, http.StatusOK) // FIXME 201 should return 201 created
|
||||
render.JSON(w, r, u)
|
||||
}
|
||||
|
||||
@@ -307,65 +307,69 @@ func (g Graph) DeleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
currentUser := revactx.ContextMustGetUser(r.Context())
|
||||
|
||||
if currentUser.GetId().GetOpaqueId() == user.GetId() {
|
||||
logger.Debug().Msg("could not delete user: self deletion forbidden")
|
||||
errorcode.NotAllowed.Render(w, r, http.StatusForbidden, "self deletion forbidden")
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debug().
|
||||
Str("user", user.GetId()).
|
||||
Msg("calling list spaces with user filter to fetch the personal space for deletion")
|
||||
opaque := utils.AppendPlainToOpaque(nil, "unrestricted", "T")
|
||||
f := listStorageSpacesUserFilter(user.GetId())
|
||||
lspr, err := g.gatewayClient.ListStorageSpaces(r.Context(), &storageprovider.ListStorageSpacesRequest{
|
||||
Opaque: opaque,
|
||||
Filters: []*storageprovider.ListStorageSpacesRequest_Filter{f},
|
||||
})
|
||||
if err != nil {
|
||||
// transport error, log as error
|
||||
logger.Error().Err(err).Msg("could not fetch spaces: transport error")
|
||||
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "could not fetch spaces for deletion, aborting")
|
||||
return
|
||||
}
|
||||
for _, sp := range lspr.GetStorageSpaces() {
|
||||
if !(sp.SpaceType == "personal" && sp.Owner.Id.OpaqueId == user.GetId()) {
|
||||
continue
|
||||
e := events.UserDeleted{UserID: user.GetId()}
|
||||
if currentUser, ok := revactx.ContextGetUser(r.Context()); ok {
|
||||
if currentUser.GetId().GetOpaqueId() == user.GetId() {
|
||||
logger.Debug().Msg("could not delete user: self deletion forbidden")
|
||||
errorcode.NotAllowed.Render(w, r, http.StatusForbidden, "self deletion forbidden")
|
||||
return
|
||||
}
|
||||
// TODO: check if request contains a homespace and if, check if requesting user has the privilege to
|
||||
// delete it and make sure it is not deleting its own homespace
|
||||
// needs modification of the cs3api
|
||||
e.Executant = currentUser.GetId()
|
||||
}
|
||||
|
||||
// Deleting a space a two step process (1. disabling/trashing, 2. purging)
|
||||
// Do the "disable/trash" step only if the space is not marked as trashed yet:
|
||||
if _, ok := sp.Opaque.Map["trashed"]; !ok {
|
||||
if g.gatewayClient != nil {
|
||||
logger.Debug().
|
||||
Str("user", user.GetId()).
|
||||
Msg("calling list spaces with user filter to fetch the personal space for deletion")
|
||||
opaque := utils.AppendPlainToOpaque(nil, "unrestricted", "T")
|
||||
f := listStorageSpacesUserFilter(user.GetId())
|
||||
lspr, err := g.gatewayClient.ListStorageSpaces(r.Context(), &storageprovider.ListStorageSpacesRequest{
|
||||
Opaque: opaque,
|
||||
Filters: []*storageprovider.ListStorageSpacesRequest_Filter{f},
|
||||
})
|
||||
if err != nil {
|
||||
// transport error, log as error
|
||||
logger.Error().Err(err).Msg("could not fetch spaces: transport error")
|
||||
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "could not fetch spaces for deletion, aborting")
|
||||
return
|
||||
}
|
||||
for _, sp := range lspr.GetStorageSpaces() {
|
||||
if !(sp.SpaceType == "personal" && sp.Owner.Id.OpaqueId == user.GetId()) {
|
||||
continue
|
||||
}
|
||||
// TODO: check if request contains a homespace and if, check if requesting user has the privilege to
|
||||
// delete it and make sure it is not deleting its own homespace
|
||||
// needs modification of the cs3api
|
||||
|
||||
// Deleting a space a two step process (1. disabling/trashing, 2. purging)
|
||||
// Do the "disable/trash" step only if the space is not marked as trashed yet:
|
||||
if _, ok := sp.Opaque.Map["trashed"]; !ok {
|
||||
_, err := g.gatewayClient.DeleteStorageSpace(r.Context(), &storageprovider.DeleteStorageSpaceRequest{
|
||||
Id: &storageprovider.StorageSpaceId{
|
||||
OpaqueId: sp.Id.OpaqueId,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not disable homespace: transport error")
|
||||
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "could not disable homespace, aborting")
|
||||
return
|
||||
}
|
||||
}
|
||||
purgeFlag := utils.AppendPlainToOpaque(nil, "purge", "")
|
||||
_, err := g.gatewayClient.DeleteStorageSpace(r.Context(), &storageprovider.DeleteStorageSpaceRequest{
|
||||
Opaque: purgeFlag,
|
||||
Id: &storageprovider.StorageSpaceId{
|
||||
OpaqueId: sp.Id.OpaqueId,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("could not disable homespace: transport error")
|
||||
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "could not disable homespace, aborting")
|
||||
// transport error, log as error
|
||||
logger.Error().Err(err).Msg("could not delete homespace: transport error")
|
||||
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "could not delete homespace, aborting")
|
||||
return
|
||||
}
|
||||
break
|
||||
}
|
||||
purgeFlag := utils.AppendPlainToOpaque(nil, "purge", "")
|
||||
_, err := g.gatewayClient.DeleteStorageSpace(r.Context(), &storageprovider.DeleteStorageSpaceRequest{
|
||||
Opaque: purgeFlag,
|
||||
Id: &storageprovider.StorageSpaceId{
|
||||
OpaqueId: sp.Id.OpaqueId,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
// transport error, log as error
|
||||
logger.Error().Err(err).Msg("could not delete homespace: transport error")
|
||||
errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, "could not delete homespace, aborting")
|
||||
return
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
logger.Debug().Str("id", user.GetId()).Msg("calling delete user on backend")
|
||||
@@ -382,7 +386,7 @@ func (g Graph) DeleteUser(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
g.publishEvent(events.UserDeleted{Executant: currentUser.Id, UserID: user.GetId()})
|
||||
g.publishEvent(e)
|
||||
|
||||
render.Status(r, http.StatusNoContent)
|
||||
render.NoContent(w, r)
|
||||
@@ -443,15 +447,16 @@ func (g Graph) PatchUser(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
currentUser := revactx.ContextMustGetUser(r.Context())
|
||||
g.publishEvent(
|
||||
events.UserFeatureChanged{
|
||||
Executant: currentUser.Id,
|
||||
UserID: nameOrID,
|
||||
Features: features,
|
||||
},
|
||||
)
|
||||
render.Status(r, http.StatusOK)
|
||||
e := events.UserFeatureChanged{
|
||||
UserID: nameOrID,
|
||||
Features: features,
|
||||
}
|
||||
if currentUser, ok := revactx.ContextGetUser(r.Context()); ok {
|
||||
e.Executant = currentUser.GetId()
|
||||
}
|
||||
g.publishEvent(e)
|
||||
|
||||
render.Status(r, http.StatusOK) // TODO StatusNoContent when prefer=minimal is used
|
||||
render.JSON(w, r, u)
|
||||
|
||||
}
|
||||
|
||||
@@ -84,7 +84,7 @@ class GraphContext implements Context {
|
||||
$displayName
|
||||
);
|
||||
$this->featureContext->setResponse($response);
|
||||
$this->featureContext->theHttpStatusCodeShouldBe(200);
|
||||
$this->featureContext->theHttpStatusCodeShouldBe(200); // TODO 204 when prefer=minimal header was sent
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user