mirror of
https://github.com/rclone/rclone.git
synced 2024-11-08 01:25:14 +01:00
cmd: add support for private repositories in serve restic
- fixes #3247
This commit is contained in:
parent
64fb4effa7
commit
939b19c3b7
@ -2,6 +2,7 @@
|
||||
package httplib
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
@ -114,6 +115,11 @@ type Server struct {
|
||||
HTMLTemplate *template.Template // HTML template for web interface
|
||||
}
|
||||
|
||||
type contextUserType struct{}
|
||||
|
||||
// ContextUserKey is a simple context key
|
||||
var ContextUserKey = &contextUserType{}
|
||||
|
||||
// singleUserProvider provides the encrypted password for a single user
|
||||
func (s *Server) singleUserProvider(user, realm string) string {
|
||||
if user == s.Opt.BasicUser {
|
||||
@ -172,6 +178,7 @@ func NewServer(handler http.Handler, opt *Options) *Server {
|
||||
}
|
||||
authenticator.RequireAuth(w, r)
|
||||
} else {
|
||||
r = r.WithContext(context.WithValue(r.Context(), ContextUserKey, username))
|
||||
oldHandler.ServeHTTP(w, r)
|
||||
}
|
||||
})
|
||||
|
@ -31,12 +31,14 @@ import (
|
||||
var (
|
||||
stdio bool
|
||||
appendOnly bool
|
||||
privateRepos bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
httpflags.AddFlags(Command.Flags())
|
||||
Command.Flags().BoolVar(&stdio, "stdio", false, "run an HTTP2 server on stdin/stdout")
|
||||
Command.Flags().BoolVar(&appendOnly, "append-only", false, "disallow deletion of repository data")
|
||||
Command.Flags().BoolVar(&privateRepos, "private-repos", false, "users can only access their private repo")
|
||||
}
|
||||
|
||||
// Command definition for cobra
|
||||
@ -116,6 +118,10 @@ these **must** end with /. Eg
|
||||
$ export RESTIC_REPOSITORY=rest:http://localhost:8080/user2repo/
|
||||
# backup user2 stuff
|
||||
|
||||
#### Private repositories ####
|
||||
|
||||
The "--private-repos" flag can be used to limit users to repositories starting
|
||||
with a path of "/<username>/".
|
||||
` + httplib.Help,
|
||||
Run: func(command *cobra.Command, args []string) {
|
||||
cmd.CheckArgs(1, 1, command, args)
|
||||
@ -209,6 +215,12 @@ func (s *server) handler(w http.ResponseWriter, r *http.Request) {
|
||||
remote := makeRemote(path)
|
||||
fs.Debugf(s.f, "%s %s", r.Method, path)
|
||||
|
||||
v := r.Context().Value(httplib.ContextUserKey)
|
||||
if privateRepos && (v == nil || !strings.HasPrefix(path, "/"+v.(string)+"/")) {
|
||||
http.Error(w, http.StatusText(http.StatusForbidden), http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
// Dispatch on path then method
|
||||
if strings.HasSuffix(path, "/") {
|
||||
switch r.Method {
|
||||
|
@ -8,61 +8,15 @@ import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ncw/rclone/cmd"
|
||||
"github.com/ncw/rclone/cmd/serve/httplib/httpflags"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// declare a few helper functions
|
||||
|
||||
// wantFunc tests the HTTP response in res and marks the test as errored if something is incorrect.
|
||||
type wantFunc func(t testing.TB, res *httptest.ResponseRecorder)
|
||||
|
||||
// newRequest returns a new HTTP request with the given params. On error, the
|
||||
// test is marked as failed.
|
||||
func newRequest(t testing.TB, method, path string, body io.Reader) *http.Request {
|
||||
req, err := http.NewRequest(method, path, body)
|
||||
require.NoError(t, err)
|
||||
return req
|
||||
}
|
||||
|
||||
// wantCode returns a function which checks that the response has the correct HTTP status code.
|
||||
func wantCode(code int) wantFunc {
|
||||
return func(t testing.TB, res *httptest.ResponseRecorder) {
|
||||
assert.Equal(t, code, res.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// wantBody returns a function which checks that the response has the data in the body.
|
||||
func wantBody(body string) wantFunc {
|
||||
return func(t testing.TB, res *httptest.ResponseRecorder) {
|
||||
assert.NotNil(t, res.Body)
|
||||
assert.Equal(t, res.Body.Bytes(), []byte(body))
|
||||
}
|
||||
}
|
||||
|
||||
// checkRequest uses f to process the request and runs the checker functions on the result.
|
||||
func checkRequest(t testing.TB, f http.HandlerFunc, req *http.Request, want []wantFunc) {
|
||||
rr := httptest.NewRecorder()
|
||||
f(rr, req)
|
||||
|
||||
for _, fn := range want {
|
||||
fn(t, rr)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRequest is a sequence of HTTP requests with (optional) tests for the response.
|
||||
type TestRequest struct {
|
||||
req *http.Request
|
||||
want []wantFunc
|
||||
}
|
||||
|
||||
// createOverwriteDeleteSeq returns a sequence which will create a new file at
|
||||
// path, and then try to overwrite and delete it.
|
||||
func createOverwriteDeleteSeq(t testing.TB, path string) []TestRequest {
|
||||
|
84
cmd/serve/restic/restic_privaterepos_test.go
Normal file
84
cmd/serve/restic/restic_privaterepos_test.go
Normal file
@ -0,0 +1,84 @@
|
||||
// +build go1.9
|
||||
|
||||
package restic
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/ncw/rclone/cmd/serve/httplib"
|
||||
|
||||
"github.com/ncw/rclone/cmd"
|
||||
"github.com/ncw/rclone/cmd/serve/httplib/httpflags"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// newAuthenticatedRequest returns a new HTTP request with the given params.
|
||||
func newAuthenticatedRequest(t testing.TB, method, path string, body io.Reader) *http.Request {
|
||||
req := newRequest(t, method, path, body)
|
||||
req = req.WithContext(context.WithValue(req.Context(), httplib.ContextUserKey, "test"))
|
||||
req.Header.Add("Accept", resticAPIV2)
|
||||
return req
|
||||
}
|
||||
|
||||
// TestResticPrivateRepositories runs tests on the restic handler code for private repositories
|
||||
func TestResticPrivateRepositories(t *testing.T) {
|
||||
buf := make([]byte, 32)
|
||||
_, err := io.ReadFull(rand.Reader, buf)
|
||||
require.NoError(t, err)
|
||||
|
||||
// setup rclone with a local backend in a temporary directory
|
||||
tempdir, err := ioutil.TempDir("", "rclone-restic-test-")
|
||||
require.NoError(t, err)
|
||||
|
||||
// make sure the tempdir is properly removed
|
||||
defer func() {
|
||||
err := os.RemoveAll(tempdir)
|
||||
require.NoError(t, err)
|
||||
}()
|
||||
|
||||
// globally set private-repos mode & test user
|
||||
prev := privateRepos
|
||||
prevUser := httpflags.Opt.BasicUser
|
||||
prevPassword := httpflags.Opt.BasicPass
|
||||
privateRepos = true
|
||||
httpflags.Opt.BasicUser = "test"
|
||||
httpflags.Opt.BasicPass = "password"
|
||||
// reset when done
|
||||
defer func() {
|
||||
privateRepos = prev
|
||||
httpflags.Opt.BasicUser = prevUser
|
||||
httpflags.Opt.BasicPass = prevPassword
|
||||
}()
|
||||
|
||||
// make a new file system in the temp dir
|
||||
f := cmd.NewFsSrc([]string{tempdir})
|
||||
srv := newServer(f, &httpflags.Opt)
|
||||
|
||||
// Requesting /test/ should allow access
|
||||
reqs := []*http.Request{
|
||||
newAuthenticatedRequest(t, "POST", "/test/?create=true", nil),
|
||||
newAuthenticatedRequest(t, "POST", "/test/config", strings.NewReader("foobar test config")),
|
||||
newAuthenticatedRequest(t, "GET", "/test/config", nil),
|
||||
}
|
||||
for _, req := range reqs {
|
||||
checkRequest(t, srv.handler, req, []wantFunc{wantCode(http.StatusOK)})
|
||||
}
|
||||
|
||||
// Requesting everything else should raise forbidden errors
|
||||
reqs = []*http.Request{
|
||||
newAuthenticatedRequest(t, "GET", "/", nil),
|
||||
newAuthenticatedRequest(t, "POST", "/other_user", nil),
|
||||
newAuthenticatedRequest(t, "GET", "/other_user/config", nil),
|
||||
}
|
||||
for _, req := range reqs {
|
||||
checkRequest(t, srv.handler, req, []wantFunc{wantCode(http.StatusForbidden)})
|
||||
}
|
||||
|
||||
}
|
57
cmd/serve/restic/restic_test_utils.go
Normal file
57
cmd/serve/restic/restic_test_utils.go
Normal file
@ -0,0 +1,57 @@
|
||||
// +build go1.9
|
||||
|
||||
package restic
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// declare a few helper functions
|
||||
|
||||
// wantFunc tests the HTTP response in res and marks the test as errored if something is incorrect.
|
||||
type wantFunc func(t testing.TB, res *httptest.ResponseRecorder)
|
||||
|
||||
// newRequest returns a new HTTP request with the given params. On error, the
|
||||
// test is marked as failed.
|
||||
func newRequest(t testing.TB, method, path string, body io.Reader) *http.Request {
|
||||
req, err := http.NewRequest(method, path, body)
|
||||
require.NoError(t, err)
|
||||
return req
|
||||
}
|
||||
|
||||
// wantCode returns a function which checks that the response has the correct HTTP status code.
|
||||
func wantCode(code int) wantFunc {
|
||||
return func(t testing.TB, res *httptest.ResponseRecorder) {
|
||||
assert.Equal(t, code, res.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// wantBody returns a function which checks that the response has the data in the body.
|
||||
func wantBody(body string) wantFunc {
|
||||
return func(t testing.TB, res *httptest.ResponseRecorder) {
|
||||
assert.NotNil(t, res.Body)
|
||||
assert.Equal(t, res.Body.Bytes(), []byte(body))
|
||||
}
|
||||
}
|
||||
|
||||
// checkRequest uses f to process the request and runs the checker functions on the result.
|
||||
func checkRequest(t testing.TB, f http.HandlerFunc, req *http.Request, want []wantFunc) {
|
||||
rr := httptest.NewRecorder()
|
||||
f(rr, req)
|
||||
|
||||
for _, fn := range want {
|
||||
fn(t, rr)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRequest is a sequence of HTTP requests with (optional) tests for the response.
|
||||
type TestRequest struct {
|
||||
req *http.Request
|
||||
want []wantFunc
|
||||
}
|
Loading…
Reference in New Issue
Block a user