mirror of
https://github.com/opencloud-eu/opencloud.git
synced 2026-01-07 04:40:05 -06: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))
|
||||
|
||||
294
extensions/graph/mocks/ldapclient.go
Normal file
294
extensions/graph/mocks/ldapclient.go
Normal file
@@ -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) {
|
||||
|
||||
88
extensions/graph/pkg/service/v0/password.go
Normal file
88
extensions/graph/pkg/service/v0/password.go
Normal file
@@ -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)
|
||||
}
|
||||
159
extensions/graph/pkg/service/v0/password_test.go
Normal file
159
extensions/graph/pkg/service/v0/password_test.go
Normal file
@@ -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)
|
||||
|
||||
6
go.mod
6
go.mod
@@ -48,7 +48,7 @@ require (
|
||||
github.com/onsi/ginkgo v1.16.5
|
||||
github.com/onsi/ginkgo/v2 v2.1.4
|
||||
github.com/onsi/gomega v1.19.0
|
||||
github.com/owncloud/libre-graph-api-go v0.13.3
|
||||
github.com/owncloud/libre-graph-api-go v0.14.2
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/prometheus/client_golang v1.12.1
|
||||
github.com/rs/zerolog v1.26.1
|
||||
@@ -67,8 +67,8 @@ require (
|
||||
go.opentelemetry.io/otel/trace v1.7.0
|
||||
golang.org/x/crypto v0.0.0-20220331220935-ae2d96664a29
|
||||
golang.org/x/image v0.0.0-20220321031419-a8550c1d254a
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f
|
||||
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a
|
||||
golang.org/x/net v0.0.0-20220516155154-20f960328961
|
||||
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5
|
||||
google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb
|
||||
google.golang.org/grpc v1.46.0
|
||||
google.golang.org/protobuf v1.28.0
|
||||
|
||||
10
go.sum
10
go.sum
@@ -1018,8 +1018,8 @@ github.com/oracle/oci-go-sdk v24.3.0+incompatible/go.mod h1:VQb79nF8Z2cwLkLS35uk
|
||||
github.com/orcaman/concurrent-map v1.0.0 h1:I/2A2XPCb4IuQWcQhBhSwGfiuybl/J0ev9HDbW65HOY=
|
||||
github.com/orcaman/concurrent-map v1.0.0/go.mod h1:Lu3tH6HLW3feq74c2GC+jIMS/K2CFcDWnWD9XkenwhI=
|
||||
github.com/ovh/go-ovh v1.1.0/go.mod h1:AxitLZ5HBRPyUd+Zl60Ajaag+rNTdVXWIkzfrVuTXWA=
|
||||
github.com/owncloud/libre-graph-api-go v0.13.3 h1:jNtQ8QcT7AZTfhdVHDaqAOs2xaJTfqfkucM9GARBIrQ=
|
||||
github.com/owncloud/libre-graph-api-go v0.13.3/go.mod h1:579sFrPP7aP24LZXGPopLfvE+hAka/2DYHk0+Ij+w+U=
|
||||
github.com/owncloud/libre-graph-api-go v0.14.2 h1:JiI32eDp7JZmiVv4aUpC4yaPpv6gK4xxM9MOe/0cXpE=
|
||||
github.com/owncloud/libre-graph-api-go v0.14.2/go.mod h1:579sFrPP7aP24LZXGPopLfvE+hAka/2DYHk0+Ij+w+U=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
|
||||
github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc=
|
||||
@@ -1485,8 +1485,9 @@ golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qx
|
||||
golang.org/x/net v0.0.0-20211020060615-d418f374d309/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f h1:oA4XRj0qtSt8Yo1Zms0CUlsT3KG69V2UGQWPBxujDmc=
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220516155154-20f960328961 h1:+W/iTMPG0EL7aW+/atntZwZrvSRIj3m3yX414dSULUU=
|
||||
golang.org/x/net v0.0.0-20220516155154-20f960328961/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
@@ -1504,8 +1505,9 @@ golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ
|
||||
golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20211005180243-6b3c2da341f1/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A=
|
||||
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a h1:qfl7ob3DIEs3Ml9oLuPwY2N04gymzAW04WsUQHIClgM=
|
||||
golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5 h1:OSnWWcOd/CtWQC2cYSBgbTSJv3ciqd8r54ySIW2y3RE=
|
||||
golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
|
||||
Reference in New Issue
Block a user