mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-04-24 13:08:26 -05:00
Add graph /me/changePassword endpoint
To allow users to change their own password when using the Graph LDAP backend. Closes: #3063
This commit is contained in:
committed by
Ralf Haferkamp
parent
cbd7ea77f7
commit
c41cf92553
@@ -28,6 +28,7 @@ ci-go-generate: $(MOCKERY) # CI runs ci-node-generate automatically before this
|
||||
$(MOCKERY) --dir pkg/service/v0 --case underscore --name GatewayClient
|
||||
$(MOCKERY) --dir pkg/service/v0 --case underscore --name HTTPClient
|
||||
$(MOCKERY) --dir pkg/service/v0 --case underscore --name Publisher
|
||||
$(MOCKERY) --srcpkg github.com/go-ldap/ldap/v3 --case underscore --filename ldapclient.go --name Client
|
||||
|
||||
|
||||
.PHONY: ci-node-generate
|
||||
|
||||
@@ -18,6 +18,36 @@ type GatewayClient struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Authenticate provides a mock function with given fields: ctx, in, opts
|
||||
func (_m *GatewayClient) Authenticate(ctx context.Context, in *gatewayv1beta1.AuthenticateRequest, opts ...grpc.CallOption) (*gatewayv1beta1.AuthenticateResponse, error) {
|
||||
_va := make([]interface{}, len(opts))
|
||||
for _i := range opts {
|
||||
_va[_i] = opts[_i]
|
||||
}
|
||||
var _ca []interface{}
|
||||
_ca = append(_ca, ctx, in)
|
||||
_ca = append(_ca, _va...)
|
||||
ret := _m.Called(_ca...)
|
||||
|
||||
var r0 *gatewayv1beta1.AuthenticateResponse
|
||||
if rf, ok := ret.Get(0).(func(context.Context, *gatewayv1beta1.AuthenticateRequest, ...grpc.CallOption) *gatewayv1beta1.AuthenticateResponse); ok {
|
||||
r0 = rf(ctx, in, opts...)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*gatewayv1beta1.AuthenticateResponse)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(context.Context, *gatewayv1beta1.AuthenticateRequest, ...grpc.CallOption) error); ok {
|
||||
r1 = rf(ctx, in, opts...)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// CreateStorageSpace provides a mock function with given fields: ctx, in, opts
|
||||
func (_m *GatewayClient) CreateStorageSpace(ctx context.Context, in *providerv1beta1.CreateStorageSpaceRequest, opts ...grpc.CallOption) (*providerv1beta1.CreateStorageSpaceResponse, error) {
|
||||
_va := make([]interface{}, len(opts))
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
// Code generated by mockery v2.10.4. DO NOT EDIT.
|
||||
|
||||
package mocks
|
||||
|
||||
import (
|
||||
ldap "github.com/go-ldap/ldap/v3"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
time "time"
|
||||
|
||||
tls "crypto/tls"
|
||||
)
|
||||
|
||||
// Client is an autogenerated mock type for the Client type
|
||||
type Client struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// Add provides a mock function with given fields: _a0
|
||||
func (_m *Client) Add(_a0 *ldap.AddRequest) error {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(*ldap.AddRequest) error); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Bind provides a mock function with given fields: username, password
|
||||
func (_m *Client) Bind(username string, password string) error {
|
||||
ret := _m.Called(username, password)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string, string) error); ok {
|
||||
r0 = rf(username, password)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Close provides a mock function with given fields:
|
||||
func (_m *Client) Close() {
|
||||
_m.Called()
|
||||
}
|
||||
|
||||
// Compare provides a mock function with given fields: dn, attribute, value
|
||||
func (_m *Client) Compare(dn string, attribute string, value string) (bool, error) {
|
||||
ret := _m.Called(dn, attribute, value)
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func(string, string, string) bool); ok {
|
||||
r0 = rf(dn, attribute, value)
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(string, string, string) error); ok {
|
||||
r1 = rf(dn, attribute, value)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Del provides a mock function with given fields: _a0
|
||||
func (_m *Client) Del(_a0 *ldap.DelRequest) error {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(*ldap.DelRequest) error); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// ExternalBind provides a mock function with given fields:
|
||||
func (_m *Client) ExternalBind() error {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func() error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// IsClosing provides a mock function with given fields:
|
||||
func (_m *Client) IsClosing() bool {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 bool
|
||||
if rf, ok := ret.Get(0).(func() bool); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Get(0).(bool)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Modify provides a mock function with given fields: _a0
|
||||
func (_m *Client) Modify(_a0 *ldap.ModifyRequest) error {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(*ldap.ModifyRequest) error); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// ModifyDN provides a mock function with given fields: _a0
|
||||
func (_m *Client) ModifyDN(_a0 *ldap.ModifyDNRequest) error {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(*ldap.ModifyDNRequest) error); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// ModifyWithResult provides a mock function with given fields: _a0
|
||||
func (_m *Client) ModifyWithResult(_a0 *ldap.ModifyRequest) (*ldap.ModifyResult, error) {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 *ldap.ModifyResult
|
||||
if rf, ok := ret.Get(0).(func(*ldap.ModifyRequest) *ldap.ModifyResult); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*ldap.ModifyResult)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(*ldap.ModifyRequest) error); ok {
|
||||
r1 = rf(_a0)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// PasswordModify provides a mock function with given fields: _a0
|
||||
func (_m *Client) PasswordModify(_a0 *ldap.PasswordModifyRequest) (*ldap.PasswordModifyResult, error) {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 *ldap.PasswordModifyResult
|
||||
if rf, ok := ret.Get(0).(func(*ldap.PasswordModifyRequest) *ldap.PasswordModifyResult); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*ldap.PasswordModifyResult)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(*ldap.PasswordModifyRequest) error); ok {
|
||||
r1 = rf(_a0)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Search provides a mock function with given fields: _a0
|
||||
func (_m *Client) Search(_a0 *ldap.SearchRequest) (*ldap.SearchResult, error) {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 *ldap.SearchResult
|
||||
if rf, ok := ret.Get(0).(func(*ldap.SearchRequest) *ldap.SearchResult); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*ldap.SearchResult)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(*ldap.SearchRequest) error); ok {
|
||||
r1 = rf(_a0)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SearchWithPaging provides a mock function with given fields: searchRequest, pagingSize
|
||||
func (_m *Client) SearchWithPaging(searchRequest *ldap.SearchRequest, pagingSize uint32) (*ldap.SearchResult, error) {
|
||||
ret := _m.Called(searchRequest, pagingSize)
|
||||
|
||||
var r0 *ldap.SearchResult
|
||||
if rf, ok := ret.Get(0).(func(*ldap.SearchRequest, uint32) *ldap.SearchResult); ok {
|
||||
r0 = rf(searchRequest, pagingSize)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*ldap.SearchResult)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(*ldap.SearchRequest, uint32) error); ok {
|
||||
r1 = rf(searchRequest, pagingSize)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// SetTimeout provides a mock function with given fields: _a0
|
||||
func (_m *Client) SetTimeout(_a0 time.Duration) {
|
||||
_m.Called(_a0)
|
||||
}
|
||||
|
||||
// SimpleBind provides a mock function with given fields: _a0
|
||||
func (_m *Client) SimpleBind(_a0 *ldap.SimpleBindRequest) (*ldap.SimpleBindResult, error) {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 *ldap.SimpleBindResult
|
||||
if rf, ok := ret.Get(0).(func(*ldap.SimpleBindRequest) *ldap.SimpleBindResult); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).(*ldap.SimpleBindResult)
|
||||
}
|
||||
}
|
||||
|
||||
var r1 error
|
||||
if rf, ok := ret.Get(1).(func(*ldap.SimpleBindRequest) error); ok {
|
||||
r1 = rf(_a0)
|
||||
} else {
|
||||
r1 = ret.Error(1)
|
||||
}
|
||||
|
||||
return r0, r1
|
||||
}
|
||||
|
||||
// Start provides a mock function with given fields:
|
||||
func (_m *Client) Start() {
|
||||
_m.Called()
|
||||
}
|
||||
|
||||
// StartTLS provides a mock function with given fields: _a0
|
||||
func (_m *Client) StartTLS(_a0 *tls.Config) error {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(*tls.Config) error); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// UnauthenticatedBind provides a mock function with given fields: username
|
||||
func (_m *Client) UnauthenticatedBind(username string) error {
|
||||
ret := _m.Called(username)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(string) error); ok {
|
||||
r0 = rf(username)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
@@ -17,12 +17,14 @@ import (
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
//go:generate make generate
|
||||
//go:generate make -C ../../.. generate
|
||||
|
||||
// GatewayClient is the subset of the gateway.GatewayAPIClient that is being used to interact with the gateway
|
||||
type GatewayClient interface {
|
||||
//gateway.GatewayAPIClient
|
||||
|
||||
// Authenticates a user.
|
||||
Authenticate(ctx context.Context, in *gateway.AuthenticateRequest, opts ...grpc.CallOption) (*gateway.AuthenticateResponse, error)
|
||||
// Returns the home path for the given authenticated user.
|
||||
// When a user has access to multiple storage providers, one of them is the home.
|
||||
GetHome(ctx context.Context, in *provider.GetHomeRequest, opts ...grpc.CallOption) (*provider.GetHomeResponse, error)
|
||||
|
||||
@@ -54,6 +54,11 @@ func (i instrument) PatchUser(w http.ResponseWriter, r *http.Request) {
|
||||
i.next.PatchUser(w, r)
|
||||
}
|
||||
|
||||
// ChangeOwnPassword implements the Service interface.
|
||||
func (i instrument) ChangeOwnPassword(w http.ResponseWriter, r *http.Request) {
|
||||
i.next.ChangeOwnPassword(w, r)
|
||||
}
|
||||
|
||||
// GetGroups implements the Service interface.
|
||||
func (i instrument) GetGroups(w http.ResponseWriter, r *http.Request) {
|
||||
i.next.GetGroups(w, r)
|
||||
|
||||
@@ -54,6 +54,11 @@ func (l logging) PatchUser(w http.ResponseWriter, r *http.Request) {
|
||||
l.next.PatchUser(w, r)
|
||||
}
|
||||
|
||||
// ChangeOwnPassword implements the Service interface.
|
||||
func (l logging) ChangeOwnPassword(w http.ResponseWriter, r *http.Request) {
|
||||
l.next.ChangeOwnPassword(w, r)
|
||||
}
|
||||
|
||||
// GetGroups implements the Service interface.
|
||||
func (l logging) GetGroups(w http.ResponseWriter, r *http.Request) {
|
||||
l.next.GetGroups(w, r)
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/cs3org/reva/v2/pkg/events"
|
||||
"github.com/owncloud/ocis/v2/extensions/graph/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/extensions/graph/pkg/identity"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/roles"
|
||||
settingssvc "github.com/owncloud/ocis/v2/protogen/gen/ocis/services/settings/v0"
|
||||
@@ -19,6 +20,7 @@ type Options struct {
|
||||
Config *config.Config
|
||||
Middleware []func(http.Handler) http.Handler
|
||||
GatewayClient GatewayClient
|
||||
IdentityBackend identity.Backend
|
||||
HTTPClient HTTPClient
|
||||
RoleService settingssvc.RoleService
|
||||
RoleManager *roles.Manager
|
||||
@@ -64,6 +66,13 @@ func WithGatewayClient(val GatewayClient) Option {
|
||||
}
|
||||
}
|
||||
|
||||
// WithIdentityBackend provides a function to set the IdentityBackend option.
|
||||
func WithIdentityBackend(val identity.Backend) Option {
|
||||
return func(o *Options) {
|
||||
o.IdentityBackend = val
|
||||
}
|
||||
}
|
||||
|
||||
// WithHTTPClient provides a function to set the http client option.
|
||||
func WithHTTPClient(val HTTPClient) Option {
|
||||
return func(o *Options) {
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
package svc
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/CiscoM31/godata"
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
cs3rpc "github.com/cs3org/go-cs3apis/cs3/rpc/v1beta1"
|
||||
revactx "github.com/cs3org/reva/v2/pkg/ctx"
|
||||
"github.com/go-chi/render"
|
||||
libregraph "github.com/owncloud/libre-graph-api-go"
|
||||
"github.com/owncloud/ocis/v2/extensions/graph/pkg/service/v0/errorcode"
|
||||
)
|
||||
|
||||
// ChangeOwnPassword implements the Service interface. It allows the user to change
|
||||
// its own password
|
||||
func (g Graph) ChangeOwnPassword(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
u, ok := revactx.ContextGetUser(ctx)
|
||||
if !ok {
|
||||
g.logger.Error().Msg("user not in context")
|
||||
errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, "user not in context")
|
||||
return
|
||||
}
|
||||
|
||||
sanitizedPath := strings.TrimPrefix(r.URL.Path, "/graph/v1.0/")
|
||||
_, err := godata.ParseRequest(r.Context(), sanitizedPath, r.URL.Query())
|
||||
if err != nil {
|
||||
g.logger.Err(err).Interface("query", r.URL.Query()).Msg("query error")
|
||||
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
cpw := libregraph.NewPasswordChange()
|
||||
err = json.NewDecoder(r.Body).Decode(cpw)
|
||||
if err != nil {
|
||||
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
currentPw := cpw.GetCurrentPassword()
|
||||
if currentPw == "" {
|
||||
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "current password cannot be empty")
|
||||
return
|
||||
}
|
||||
|
||||
newPw := cpw.GetNewPassword()
|
||||
if newPw == "" {
|
||||
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "new password cannot be empty")
|
||||
return
|
||||
}
|
||||
|
||||
if newPw == currentPw {
|
||||
errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "new password must be differnt from current password")
|
||||
return
|
||||
}
|
||||
|
||||
authReq := &gateway.AuthenticateRequest{
|
||||
Type: "basic",
|
||||
ClientId: u.Username,
|
||||
ClientSecret: currentPw,
|
||||
}
|
||||
authRes, err := g.gatewayClient.Authenticate(r.Context(), authReq)
|
||||
if err != nil {
|
||||
errorcode.ServiceNotAvailable.Render(w, r, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if authRes.Status.Code != cs3rpc.Code_CODE_OK {
|
||||
errorcode.InvalidRequest.Render(w, r, http.StatusInternalServerError, "password change failed")
|
||||
return
|
||||
}
|
||||
|
||||
newPwProfile := libregraph.NewPasswordProfile()
|
||||
newPwProfile.SetPassword(newPw)
|
||||
changes := libregraph.NewUser()
|
||||
changes.SetPasswordProfile(*newPwProfile)
|
||||
_, err = g.identityBackend.UpdateUser(ctx, u.Id.OpaqueId, *changes)
|
||||
if err != nil {
|
||||
errorcode.InvalidRequest.Render(w, r, http.StatusInternalServerError, "password change failed")
|
||||
g.logger.Debug().Err(err).Str("userid", u.Id.OpaqueId).Msg("failed to update user password")
|
||||
return
|
||||
}
|
||||
|
||||
render.Status(r, http.StatusNoContent)
|
||||
render.NoContent(w, r)
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
package svc_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
|
||||
gateway "github.com/cs3org/go-cs3apis/cs3/gateway/v1beta1"
|
||||
userv1beta1 "github.com/cs3org/go-cs3apis/cs3/identity/user/v1beta1"
|
||||
revactx "github.com/cs3org/reva/v2/pkg/ctx"
|
||||
"github.com/cs3org/reva/v2/pkg/rgrpc/status"
|
||||
"github.com/go-ldap/ldap/v3"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
libregraph "github.com/owncloud/libre-graph-api-go"
|
||||
"github.com/owncloud/ocis/v2/extensions/graph/mocks"
|
||||
"github.com/owncloud/ocis/v2/extensions/graph/pkg/config"
|
||||
"github.com/owncloud/ocis/v2/extensions/graph/pkg/config/defaults"
|
||||
"github.com/owncloud/ocis/v2/extensions/graph/pkg/identity"
|
||||
service "github.com/owncloud/ocis/v2/extensions/graph/pkg/service/v0"
|
||||
"github.com/owncloud/ocis/v2/ocis-pkg/log"
|
||||
"github.com/stretchr/testify/mock"
|
||||
)
|
||||
|
||||
type changePwTest struct {
|
||||
desc string
|
||||
currentpw string
|
||||
newpw string
|
||||
expected int
|
||||
}
|
||||
|
||||
var _ = Describe("Users changing their own password", func() {
|
||||
var (
|
||||
svc service.Service
|
||||
gatewayClient *mocks.GatewayClient
|
||||
httpClient *mocks.HTTPClient
|
||||
ldapClient *mocks.Client
|
||||
ldapConfig config.LDAP
|
||||
identityBackend identity.Backend
|
||||
eventsPublisher mocks.Publisher
|
||||
ctx context.Context
|
||||
cfg *config.Config
|
||||
user *userv1beta1.User
|
||||
err error
|
||||
)
|
||||
|
||||
JustBeforeEach(func() {
|
||||
ctx = context.Background()
|
||||
cfg = defaults.FullDefaultConfig()
|
||||
cfg.TokenManager.JWTSecret = "loremipsum"
|
||||
|
||||
gatewayClient = &mocks.GatewayClient{}
|
||||
ldapClient = mockedLDAPClient()
|
||||
|
||||
ldapConfig = config.LDAP{
|
||||
WriteEnabled: true,
|
||||
UserDisplayNameAttribute: "displayName",
|
||||
UserNameAttribute: "uid",
|
||||
UserEmailAttribute: "mail",
|
||||
UserIDAttribute: "ownclouduuid",
|
||||
UserSearchScope: "sub",
|
||||
GroupNameAttribute: "cn",
|
||||
GroupIDAttribute: "ownclouduui",
|
||||
GroupSearchScope: "sub",
|
||||
}
|
||||
loggger := log.NewLogger()
|
||||
identityBackend, err = identity.NewLDAPBackend(ldapClient, ldapConfig, &loggger)
|
||||
Expect(err).To(BeNil())
|
||||
|
||||
httpClient = &mocks.HTTPClient{}
|
||||
eventsPublisher = mocks.Publisher{}
|
||||
svc = service.NewService(
|
||||
service.Config(cfg),
|
||||
service.WithGatewayClient(gatewayClient),
|
||||
service.WithIdentityBackend(identityBackend),
|
||||
service.WithHTTPClient(httpClient),
|
||||
service.EventsPublisher(&eventsPublisher),
|
||||
)
|
||||
user = &userv1beta1.User{
|
||||
Id: &userv1beta1.UserId{
|
||||
OpaqueId: "user",
|
||||
},
|
||||
}
|
||||
ctx = revactx.ContextSetUser(ctx, user)
|
||||
})
|
||||
|
||||
It("fails if no user in context", func() {
|
||||
r := httptest.NewRequest(http.MethodGet, "/graph/v1.0/me/changePassword", nil)
|
||||
rr := httptest.NewRecorder()
|
||||
svc.ChangeOwnPassword(rr, r)
|
||||
Expect(rr.Code).To(Equal(http.StatusInternalServerError))
|
||||
})
|
||||
|
||||
DescribeTable("changing the password",
|
||||
func(current string, newpw string, authresult string, expected int) {
|
||||
switch authresult {
|
||||
case "error":
|
||||
gatewayClient.On("Authenticate", mock.Anything, mock.Anything).Return(nil, errors.New("fail"))
|
||||
case "deny":
|
||||
gatewayClient.On("Authenticate", mock.Anything, mock.Anything).Return(&gateway.AuthenticateResponse{
|
||||
Status: status.NewPermissionDenied(ctx, errors.New("wrong password"), "wrong password"),
|
||||
Token: "authtoken",
|
||||
}, nil)
|
||||
default:
|
||||
gatewayClient.On("Authenticate", mock.Anything, mock.Anything).Return(&gateway.AuthenticateResponse{
|
||||
Status: status.NewOK(ctx),
|
||||
Token: "authtoken",
|
||||
}, nil)
|
||||
}
|
||||
cpw := libregraph.NewPasswordChange()
|
||||
cpw.SetCurrentPassword(current)
|
||||
cpw.SetNewPassword(newpw)
|
||||
body, _ := json.Marshal(cpw)
|
||||
b := bytes.NewBuffer(body)
|
||||
r := httptest.NewRequest(http.MethodPost, "/graph/v1.0/me/changePassword", b).WithContext(ctx)
|
||||
rr := httptest.NewRecorder()
|
||||
svc.ChangeOwnPassword(rr, r)
|
||||
Expect(rr.Code).To(Equal(expected))
|
||||
},
|
||||
Entry("fails when current password is empty", "", "newpassword", "", http.StatusBadRequest),
|
||||
Entry("fails when new password is empty", "currentpassword", "", "", http.StatusBadRequest),
|
||||
Entry("fails when current and new password are equal", "password", "password", "", http.StatusBadRequest),
|
||||
Entry("fails authentication with current password errors", "currentpassword", "newpassword", "error", http.StatusInternalServerError),
|
||||
Entry("fails when current password is wrong", "currentpassword", "newpassword", "deny", http.StatusInternalServerError),
|
||||
Entry("succeeds when current password is correct", "currentpassword", "newpassword", "", http.StatusNoContent),
|
||||
)
|
||||
})
|
||||
|
||||
func mockedLDAPClient() *mocks.Client {
|
||||
lm := &mocks.Client{}
|
||||
|
||||
userEntry := ldap.NewEntry("uid=test", map[string][]string{
|
||||
"uid": {"test"},
|
||||
"displayName": {"test"},
|
||||
"mail": {"test@example.org"},
|
||||
})
|
||||
|
||||
lm.On("Search", mock.Anything, mock.Anything, mock.Anything, mock.Anything,
|
||||
mock.Anything, mock.Anything, mock.Anything, mock.Anything, mock.Anything).
|
||||
Return(
|
||||
&ldap.SearchResult{Entries: []*ldap.Entry{userEntry}},
|
||||
nil)
|
||||
|
||||
mr := ldap.NewModifyRequest("uid=test", nil)
|
||||
mr.Changes = []ldap.Change{
|
||||
{
|
||||
Operation: ldap.ReplaceAttribute,
|
||||
Modification: ldap.PartialAttribute{
|
||||
Type: "userPassword",
|
||||
Vals: []string{"newpassword"},
|
||||
},
|
||||
},
|
||||
}
|
||||
lm.On("Modify", mr).Return(nil)
|
||||
return lm
|
||||
}
|
||||
@@ -35,6 +35,7 @@ type Service interface {
|
||||
PostUser(http.ResponseWriter, *http.Request)
|
||||
DeleteUser(http.ResponseWriter, *http.Request)
|
||||
PatchUser(http.ResponseWriter, *http.Request)
|
||||
ChangeOwnPassword(http.ResponseWriter, *http.Request)
|
||||
|
||||
GetGroups(http.ResponseWriter, *http.Request)
|
||||
GetGroup(http.ResponseWriter, *http.Request)
|
||||
@@ -55,46 +56,10 @@ func NewService(opts ...Option) Service {
|
||||
m := chi.NewMux()
|
||||
m.Use(options.Middleware...)
|
||||
|
||||
var backend identity.Backend
|
||||
switch options.Config.Identity.Backend {
|
||||
case "cs3":
|
||||
backend = &identity.CS3{
|
||||
Config: options.Config.Reva,
|
||||
Logger: &options.Logger,
|
||||
}
|
||||
case "ldap":
|
||||
var err error
|
||||
|
||||
var tlsConf *tls.Config
|
||||
if options.Config.Identity.LDAP.Insecure {
|
||||
tlsConf = &tls.Config{
|
||||
//nolint:gosec // We need the ability to run with "insecure" (dev/testing)
|
||||
InsecureSkipVerify: options.Config.Identity.LDAP.Insecure,
|
||||
}
|
||||
}
|
||||
|
||||
conn := ldap.NewLDAPWithReconnect(&options.Logger,
|
||||
ldap.Config{
|
||||
URI: options.Config.Identity.LDAP.URI,
|
||||
BindDN: options.Config.Identity.LDAP.BindDN,
|
||||
BindPassword: options.Config.Identity.LDAP.BindPassword,
|
||||
TLSConfig: tlsConf,
|
||||
},
|
||||
)
|
||||
if backend, err = identity.NewLDAPBackend(conn, options.Config.Identity.LDAP, &options.Logger); err != nil {
|
||||
options.Logger.Error().Msgf("Error initializing LDAP Backend: '%s'", err)
|
||||
return nil
|
||||
}
|
||||
default:
|
||||
options.Logger.Error().Msgf("Unknown Identity Backend: '%s'", options.Config.Identity.Backend)
|
||||
return nil
|
||||
}
|
||||
|
||||
svc := Graph{
|
||||
config: options.Config,
|
||||
mux: m,
|
||||
logger: &options.Logger,
|
||||
identityBackend: backend,
|
||||
spacePropertiesCache: ttlcache.NewCache(),
|
||||
eventsPublisher: options.EventsPublisher,
|
||||
}
|
||||
@@ -108,6 +73,44 @@ func NewService(opts ...Option) Service {
|
||||
} else {
|
||||
svc.gatewayClient = options.GatewayClient
|
||||
}
|
||||
if options.IdentityBackend == nil {
|
||||
switch options.Config.Identity.Backend {
|
||||
case "cs3":
|
||||
svc.identityBackend = &identity.CS3{
|
||||
Config: options.Config.Reva,
|
||||
Logger: &options.Logger,
|
||||
}
|
||||
case "ldap":
|
||||
var err error
|
||||
|
||||
var tlsConf *tls.Config
|
||||
if options.Config.Identity.LDAP.Insecure {
|
||||
tlsConf = &tls.Config{
|
||||
//nolint:gosec // We need the ability to run with "insecure" (dev/testing)
|
||||
InsecureSkipVerify: options.Config.Identity.LDAP.Insecure,
|
||||
}
|
||||
}
|
||||
|
||||
conn := ldap.NewLDAPWithReconnect(&options.Logger,
|
||||
ldap.Config{
|
||||
URI: options.Config.Identity.LDAP.URI,
|
||||
BindDN: options.Config.Identity.LDAP.BindDN,
|
||||
BindPassword: options.Config.Identity.LDAP.BindPassword,
|
||||
TLSConfig: tlsConf,
|
||||
},
|
||||
)
|
||||
if svc.identityBackend, err = identity.NewLDAPBackend(conn, options.Config.Identity.LDAP, &options.Logger); err != nil {
|
||||
options.Logger.Error().Msgf("Error initializing LDAP Backend: '%s'", err)
|
||||
return nil
|
||||
}
|
||||
default:
|
||||
options.Logger.Error().Msgf("Unknown Identity Backend: '%s'", options.Config.Identity.Backend)
|
||||
return nil
|
||||
}
|
||||
} else {
|
||||
svc.identityBackend = options.IdentityBackend
|
||||
}
|
||||
|
||||
if options.HTTPClient == nil {
|
||||
http.DefaultTransport.(*http.Transport).TLSClientConfig = &tls.Config{
|
||||
InsecureSkipVerify: options.Config.Spaces.Insecure, //nolint:gosec
|
||||
@@ -143,6 +146,7 @@ func NewService(opts ...Option) Service {
|
||||
r.Get("/", svc.GetMe)
|
||||
r.Get("/drives", svc.GetDrives)
|
||||
r.Get("/drive/root/children", svc.GetRootDriveChildren)
|
||||
r.Post("/changePassword", svc.ChangeOwnPassword)
|
||||
})
|
||||
r.Route("/users", func(r chi.Router) {
|
||||
r.With(requireAdmin).Get("/", svc.GetUsers)
|
||||
|
||||
@@ -50,6 +50,11 @@ func (t tracing) PatchUser(w http.ResponseWriter, r *http.Request) {
|
||||
t.next.PatchUser(w, r)
|
||||
}
|
||||
|
||||
// ChangeOwnPassword implements the Service interface.
|
||||
func (t tracing) ChangeOwnPassword(w http.ResponseWriter, r *http.Request) {
|
||||
t.next.ChangeOwnPassword(w, r)
|
||||
}
|
||||
|
||||
// GetGroups implements the Service interface.
|
||||
func (t tracing) GetGroups(w http.ResponseWriter, r *http.Request) {
|
||||
t.next.GetGroups(w, r)
|
||||
|
||||
Reference in New Issue
Block a user