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
|
package httplib
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
@ -114,6 +115,11 @@ type Server struct {
|
|||||||
HTMLTemplate *template.Template // HTML template for web interface
|
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
|
// singleUserProvider provides the encrypted password for a single user
|
||||||
func (s *Server) singleUserProvider(user, realm string) string {
|
func (s *Server) singleUserProvider(user, realm string) string {
|
||||||
if user == s.Opt.BasicUser {
|
if user == s.Opt.BasicUser {
|
||||||
@ -172,6 +178,7 @@ func NewServer(handler http.Handler, opt *Options) *Server {
|
|||||||
}
|
}
|
||||||
authenticator.RequireAuth(w, r)
|
authenticator.RequireAuth(w, r)
|
||||||
} else {
|
} else {
|
||||||
|
r = r.WithContext(context.WithValue(r.Context(), ContextUserKey, username))
|
||||||
oldHandler.ServeHTTP(w, r)
|
oldHandler.ServeHTTP(w, r)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -31,12 +31,14 @@ import (
|
|||||||
var (
|
var (
|
||||||
stdio bool
|
stdio bool
|
||||||
appendOnly bool
|
appendOnly bool
|
||||||
|
privateRepos bool
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
httpflags.AddFlags(Command.Flags())
|
httpflags.AddFlags(Command.Flags())
|
||||||
Command.Flags().BoolVar(&stdio, "stdio", false, "run an HTTP2 server on stdin/stdout")
|
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(&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
|
// Command definition for cobra
|
||||||
@ -116,6 +118,10 @@ these **must** end with /. Eg
|
|||||||
$ export RESTIC_REPOSITORY=rest:http://localhost:8080/user2repo/
|
$ export RESTIC_REPOSITORY=rest:http://localhost:8080/user2repo/
|
||||||
# backup user2 stuff
|
# 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,
|
` + httplib.Help,
|
||||||
Run: func(command *cobra.Command, args []string) {
|
Run: func(command *cobra.Command, args []string) {
|
||||||
cmd.CheckArgs(1, 1, command, args)
|
cmd.CheckArgs(1, 1, command, args)
|
||||||
@ -209,6 +215,12 @@ func (s *server) handler(w http.ResponseWriter, r *http.Request) {
|
|||||||
remote := makeRemote(path)
|
remote := makeRemote(path)
|
||||||
fs.Debugf(s.f, "%s %s", r.Method, 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
|
// Dispatch on path then method
|
||||||
if strings.HasSuffix(path, "/") {
|
if strings.HasSuffix(path, "/") {
|
||||||
switch r.Method {
|
switch r.Method {
|
||||||
|
@ -8,61 +8,15 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/ncw/rclone/cmd"
|
"github.com/ncw/rclone/cmd"
|
||||||
"github.com/ncw/rclone/cmd/serve/httplib/httpflags"
|
"github.com/ncw/rclone/cmd/serve/httplib/httpflags"
|
||||||
"github.com/stretchr/testify/assert"
|
|
||||||
"github.com/stretchr/testify/require"
|
"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
|
// createOverwriteDeleteSeq returns a sequence which will create a new file at
|
||||||
// path, and then try to overwrite and delete it.
|
// path, and then try to overwrite and delete it.
|
||||||
func createOverwriteDeleteSeq(t testing.TB, path string) []TestRequest {
|
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