diff --git a/README.md b/README.md index e275701f..a90a5f03 100644 --- a/README.md +++ b/README.md @@ -133,8 +133,8 @@ _this is the recommended way to run remark42_ | edit-time | EDIT_TIME | `5m` | edit window | | read-age | READONLY_AGE | | read-only age of comments, days | | img-proxy | IMG_PROXY | `false` | enable http->https proxy for images | +| admin-passwd | ADMIN_PASSWD | | password for `admin` basic auth | | dbg | DEBUG | `false` | debug mode | -| dev-passwd | DEV_PASSWD | | password for `dev` user | * command line parameters are long form `--=value`, i.e. `--site=https://demo.remark42.com` * _multi_ parameters separated by `,` in the environment or repeated with command line key, like `--site=s1 --site=s2 ...` diff --git a/backend/Gopkg.lock b/backend/Gopkg.lock index 7beda51c..9121d90d 100644 --- a/backend/Gopkg.lock +++ b/backend/Gopkg.lock @@ -113,7 +113,7 @@ [[projects]] branch = "master" - digest = "1:5ef69525e5e62fb771f3f6910c94030a86b542eff1cb9d9350803b6dae147144" + digest = "1:b117a0a0b46dad26254a48c11a511d6c697038e591a1d7ce11a229e1c8e0a237" name = "github.com/go-pkgz/auth" packages = [ ".", @@ -123,7 +123,7 @@ "token", ] pruneopts = "UT" - revision = "8d5238712a320d972f9d658e2fd1d4468ef81c3e" + revision = "b95cb645615503dba4d5fced3b77d97d4f0dcc81" [[projects]] digest = "1:1212e114344a5cdcc834ea69e19d456eef230f9784659080fee67e02ba2cb574" @@ -145,7 +145,7 @@ version = "v1.0.0" [[projects]] - digest = "1:71dc1e5b19e179495d2e2ca63454a9204753c5ecb3faa4a842ea5859355a968f" + digest = "1:e133aa7be09588b02198e4ddb98df5033b0319b56533881d0163ee51b903305b" name = "github.com/go-pkgz/rest" packages = [ ".", @@ -153,8 +153,8 @@ "logger", ] pruneopts = "UT" - revision = "c0e09a7a640e54001aed8bad117d60ad8971958e" - version = "v1.1.1" + revision = "553c0e1b55b215f8f55da4682ac57aff9aec8b6d" + version = "v1.1.5" [[projects]] digest = "1:ffc060c551980d37ee9e428ef528ee2813137249ccebb0bfc412ef83071cac91" diff --git a/backend/app/cmd/server.go b/backend/app/cmd/server.go index 9d983c69..62951d2c 100644 --- a/backend/app/cmd/server.go +++ b/backend/app/cmd/server.go @@ -43,7 +43,7 @@ type ServerCommand struct { SSL SSLGroup `group:"ssl" namespace:"ssl" env-namespace:"SSL"` Sites []string `long:"site" env:"SITE" default:"remark" description:"site names" env-delim:","` - DevPasswd string `long:"dev-passwd" env:"DEV_PASSWD" default:"" description:"development mode password"` + AdminPasswd string `long:"admin-passwd" env:"ADMIN_PASSWD" default:"" description:"admin basic auth password"` BackupLocation string `long:"backup" env:"BACKUP_PATH" default:"./var/backup" description:"backups location"` MaxBackupFiles int `long:"max-back" env:"MAX_BACKUP_FILES" default:"10" description:"max backups to keep"` ImageProxy bool `long:"img-proxy" env:"IMG_PROXY" description:"enable image proxy"` @@ -293,8 +293,8 @@ func (s *ServerCommand) newServerApp() (*serverApp, error) { // Run all application objects func (a *serverApp) run(ctx context.Context) error { - if a.DevPasswd != "" { - log.Printf("[WARN] running in dev mode") + if a.AdminPasswd != "" { + log.Printf("[WARN] admin basic auth enabled") } go func() { @@ -537,7 +537,7 @@ func (s *ServerCommand) makeAuthenticator(ds *service.DataStore, avas avatar.Sto c.User.SetBoolAttr("blocked", ds.IsBlocked(c.Audience, c.User.ID)) return c }), - DevPasswd: s.DevPasswd, + AdminPasswd: s.AdminPasswd, Validator: token.ValidatorFunc(func(token string, claims token.Claims) bool { // check on each auth call (in middleware) if claims.User == nil { return false diff --git a/backend/app/cmd/server_test.go b/backend/app/cmd/server_test.go index 9fcfb546..c73985cf 100644 --- a/backend/app/cmd/server_test.go +++ b/backend/app/cmd/server_test.go @@ -42,7 +42,7 @@ func TestServerApp(t *testing.T) { client := http.Client{Timeout: 5 * time.Second} req, err := http.NewRequest("POST", "http://localhost:18080/api/v1/comment", strings.NewReader(`{"text": "test 123", "locator":{"url": "https://radio-t.com/blah1", "site": "remark"}}`)) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") require.Nil(t, err) resp, err = client.Do(req) require.Nil(t, err) @@ -58,7 +58,7 @@ func TestServerApp(t *testing.T) { func TestServerApp_DevMode(t *testing.T) { app, ctx := prepServerApp(t, 500*time.Millisecond, func(o ServerCommand) ServerCommand { o.Port = 18085 - o.DevPasswd = "password" + o.AdminPasswd = "password" o.Auth.Dev = true return o }) @@ -95,7 +95,7 @@ func TestServerApp_WithMongo(t *testing.T) { // prepare options p := flags.NewParser(&opts, flags.Default) - _, err := p.ParseArgs([]string{"--dev-passwd=password", "--cache.type=none", "--store.type=mongo", + _, err := p.ParseArgs([]string{"--admin-passwd=password", "--cache.type=none", "--store.type=mongo", "--avatar.type=mongo", "--mongo.url=" + mongoURL, "--mongo.db=test_remark", "--port=12345", "--admin.type=mongo"}) require.Nil(t, err) opts.Auth.Github.CSEC, opts.Auth.Github.CID = "csec", "cid" @@ -142,7 +142,7 @@ func TestServerApp_WithSSL(t *testing.T) { // prepare options p := flags.NewParser(&opts, flags.Default) - _, err := p.ParseArgs([]string{"--dev-passwd=password", "--port=18080", "--store.bolt.path=/tmp/xyz", "--backup=/tmp", "--avatar.type=bolt", "--avatar.bolt.file=/tmp/ava-test.db", "--notify.type=none", + _, err := p.ParseArgs([]string{"--admin-passwd=password", "--port=18080", "--store.bolt.path=/tmp/xyz", "--backup=/tmp", "--avatar.type=bolt", "--avatar.bolt.file=/tmp/ava-test.db", "--notify.type=none", "--ssl.type=static", "--ssl.cert=testdata/cert.pem", "--ssl.key=testdata/key.pem", "--ssl.port=18443"}) require.Nil(t, err) @@ -318,7 +318,7 @@ func prepServerApp(t *testing.T, duration time.Duration, fn func(o ServerCommand // prepare options p := flags.NewParser(&cmd, flags.Default) - _, err := p.ParseArgs([]string{"--dev-passwd=password"}) + _, err := p.ParseArgs([]string{"--admin-passwd=password"}) require.Nil(t, err) cmd.Avatar.FS.Path, cmd.Avatar.Type, cmd.BackupLocation = "/tmp", "fs", "/tmp" cmd.Store.Bolt.Path = fmt.Sprintf("/tmp/%d", cmd.Port) diff --git a/backend/app/rest/api/admin.go b/backend/app/rest/api/admin.go index 34db4411..b4a60988 100644 --- a/backend/app/rest/api/admin.go +++ b/backend/app/rest/api/admin.go @@ -120,7 +120,7 @@ func (a *admin) deleteMeRequestCtrl(w http.ResponseWriter, r *http.Request) { return } - if claims.User.Picture != "" { + if claims.User.Picture != "" && a.authenticator.AvatarProxy() != nil { avatartStore := a.authenticator.AvatarProxy().Store if err := avatartStore.Remove(path.Base(claims.User.Picture)); err != nil { rest.SendErrorJSON(w, r, http.StatusBadRequest, err, "can't delete user's avatar") diff --git a/backend/app/rest/api/admin_test.go b/backend/app/rest/api/admin_test.go index 9ebb64af..002e6fcd 100644 --- a/backend/app/rest/api/admin_test.go +++ b/backend/app/rest/api/admin_test.go @@ -23,9 +23,8 @@ import ( ) func TestAdmin_Delete(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", User: store.User{ID: "id", Name: "name"}, Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}} @@ -39,12 +38,12 @@ func TestAdmin_Delete(t *testing.T) { req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/api/v1/admin/comment/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, id1), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") resp, err := client.Do(req) assert.Nil(t, err) assert.Equal(t, 200, resp.StatusCode) - body, code := getWithAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, id1)) + body, code := getWithDevAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, id1)) assert.Equal(t, 200, code) cr := store.Comment{} err = json.Unmarshal([]byte(body), &cr) @@ -54,9 +53,8 @@ func TestAdmin_Delete(t *testing.T) { } func TestAdmin_DeleteUser(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", Orig: "o test test #1", User: store.User{ID: "id1", Name: "name"}, Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}} @@ -76,7 +74,7 @@ func TestAdmin_DeleteUser(t *testing.T) { client := http.Client{} req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/api/v1/admin/user/%s?site=radio-t", ts.URL, "id2"), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") resp, err := client.Do(req) assert.Nil(t, err) assert.Equal(t, 200, resp.StatusCode) @@ -108,9 +106,8 @@ func TestAdmin_DeleteUser(t *testing.T) { } func TestAdmin_Pin(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}} @@ -125,7 +122,7 @@ func TestAdmin_Pin(t *testing.T) { req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/api/v1/admin/pin/%s?site=radio-t&url=https://radio-t.com/blah&pin=%d", ts.URL, id1, val), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") resp, err := client.Do(req) assert.Nil(t, err) return resp.StatusCode @@ -152,9 +149,8 @@ func TestAdmin_Pin(t *testing.T) { } func TestAdmin_Block(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}, User: store.User{Name: "user1 name", ID: "user1"}} @@ -174,7 +170,7 @@ func TestAdmin_Block(t *testing.T) { } req, e := http.NewRequest(http.MethodPut, url, nil) assert.Nil(t, e) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") resp, e := client.Do(req) require.Nil(t, e) body, e = ioutil.ReadAll(resp.Body) @@ -233,9 +229,8 @@ func TestAdmin_Block(t *testing.T) { } func TestAdmin_BlockedList(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() client := http.Client{} @@ -243,7 +238,7 @@ func TestAdmin_BlockedList(t *testing.T) { req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/api/v1/admin/user/%s?site=radio-t&block=%d", ts.URL, "user1", 1), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") _, err = client.Do(req) require.Nil(t, err) @@ -251,33 +246,40 @@ func TestAdmin_BlockedList(t *testing.T) { req, err = http.NewRequest(http.MethodPut, fmt.Sprintf("%s/api/v1/admin/user/%s?site=radio-t&block=%d&ttl=50ms", ts.URL, "user2", 1), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") _, err = client.Do(req) require.Nil(t, err) - res, code := getWithAuth(t, ts.URL+"/api/v1/admin/blocked?site=radio-t") - require.Equal(t, 200, code, res) + req, err = http.NewRequest("GET", ts.URL+"/api/v1/admin/blocked?site=radio-t", nil) + require.Nil(t, err) + req.SetBasicAuth("admin", "password") + res, err := client.Do(req) + require.Nil(t, err) + require.Equal(t, 200, res.StatusCode) users := []store.BlockedUser{} - err = json.Unmarshal([]byte(res), &users) + err = json.NewDecoder(res.Body).Decode(&users) assert.Nil(t, err) assert.Equal(t, 2, len(users), "two users blocked") assert.Equal(t, "user1", users[0].ID) assert.Equal(t, "user2", users[1].ID) time.Sleep(50 * time.Millisecond) - res, code = getWithAuth(t, ts.URL+"/api/v1/admin/blocked?site=radio-t") - require.Equal(t, 200, code, res) + + req, err = http.NewRequest("GET", ts.URL+"/api/v1/admin/blocked?site=radio-t", nil) + require.Nil(t, err) + req.SetBasicAuth("admin", "password") + res, err = client.Do(req) + require.Equal(t, 200, res.StatusCode) users = []store.BlockedUser{} - err = json.Unmarshal([]byte(res), &users) + err = json.NewDecoder(res.Body).Decode(&users) assert.Nil(t, err) assert.Equal(t, 1, len(users), "one user left blocked") } func TestAdmin_ReadOnly(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}, User: store.User{Name: "user1 name", ID: "user1"}} @@ -299,7 +301,7 @@ func TestAdmin_ReadOnly(t *testing.T) { req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/api/v1/admin/readonly?site=radio-t&url=https://radio-t.com/blah&ro=1", ts.URL), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") resp, err := client.Do(req) require.Nil(t, err) assert.Equal(t, 200, resp.StatusCode) @@ -314,7 +316,7 @@ func TestAdmin_ReadOnly(t *testing.T) { assert.Nil(t, err, "can't marshal comment %+v", c) req, err = http.NewRequest("POST", ts.URL+"/api/v1/comment", bytes.NewBuffer(b)) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") resp, err = client.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusForbidden, resp.StatusCode) @@ -323,7 +325,7 @@ func TestAdmin_ReadOnly(t *testing.T) { req, err = http.NewRequest(http.MethodPut, fmt.Sprintf("%s/api/v1/admin/readonly?site=radio-t&url=https://radio-t.com/blah&ro=0", ts.URL), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") resp, err = client.Do(req) assert.Equal(t, 200, resp.StatusCode) require.Nil(t, err) @@ -338,16 +340,15 @@ func TestAdmin_ReadOnly(t *testing.T) { assert.Nil(t, err, "can't marshal comment %+v", c) req, err = http.NewRequest("POST", ts.URL+"/api/v1/comment", bytes.NewBuffer(b)) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") resp, err = client.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusCreated, resp.StatusCode) } func TestAdmin_ReadOnlyWithAge(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}, User: store.User{Name: "user1 name", ID: "user1"}, @@ -365,7 +366,7 @@ func TestAdmin_ReadOnlyWithAge(t *testing.T) { req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/api/v1/admin/readonly?site=radio-t&url=https://radio-t.com/blah&ro=1", ts.URL), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") resp, err := client.Do(req) require.Nil(t, err) assert.Equal(t, 200, resp.StatusCode) @@ -377,7 +378,7 @@ func TestAdmin_ReadOnlyWithAge(t *testing.T) { req, err = http.NewRequest(http.MethodPut, fmt.Sprintf("%s/api/v1/admin/readonly?site=radio-t&url=https://radio-t.com/blah&ro=0", ts.URL), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") resp, err = client.Do(req) assert.Equal(t, 403, resp.StatusCode) require.Nil(t, err) @@ -387,9 +388,8 @@ func TestAdmin_ReadOnlyWithAge(t *testing.T) { } func TestAdmin_Verify(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}, User: store.User{Name: "user1 name", ID: "user1"}} @@ -408,7 +408,7 @@ func TestAdmin_Verify(t *testing.T) { req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/api/v1/admin/verify/user1?site=radio-t&verified=1", ts.URL), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") _, err = client.Do(req) require.Nil(t, err) verified = srv.DataService.IsVerified("radio-t", "user1") @@ -426,7 +426,7 @@ func TestAdmin_Verify(t *testing.T) { req, err = http.NewRequest(http.MethodPut, fmt.Sprintf("%s/api/v1/admin/verify/user1?site=radio-t&verified=0", ts.URL), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") _, err = client.Do(req) require.Nil(t, err) verified = srv.DataService.IsVerified("radio-t", "user1") @@ -443,9 +443,8 @@ func TestAdmin_Verify(t *testing.T) { } func TestAdmin_ExportStream(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} @@ -455,7 +454,7 @@ func TestAdmin_ExportStream(t *testing.T) { addComment(t, c1, ts) addComment(t, c2, ts) - body, code := getWithAuth(t, ts.URL+"/api/v1/admin/export?site=radio-t&mode=stream") + body, code := getWithAdminAuth(t, ts.URL+"/api/v1/admin/export?site=radio-t&mode=stream") assert.Equal(t, 200, code) assert.Equal(t, 3, strings.Count(body, "\n")) assert.Equal(t, 2, strings.Count(body, "\"text\"")) @@ -463,9 +462,8 @@ func TestAdmin_ExportStream(t *testing.T) { } func TestAdmin_ExportFile(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} @@ -478,7 +476,7 @@ func TestAdmin_ExportFile(t *testing.T) { client := &http.Client{Timeout: 5 * time.Second} req, err := http.NewRequest("GET", ts.URL+"/api/v1/admin/export?site=radio-t&mode=file", nil) require.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") resp, err := client.Do(req) require.Nil(t, err) @@ -495,9 +493,8 @@ func TestAdmin_ExportFile(t *testing.T) { } func TestAdmin_DeleteMeRequest(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}, User: store.User{Name: "user1 name", ID: "user1"}} @@ -531,9 +528,8 @@ func TestAdmin_DeleteMeRequest(t *testing.T) { }, } - _ = os.MkdirAll("/tmp/42", 0700) - defer func() { _ = os.RemoveAll("/tmp/42") }() - require.NoError(t, ioutil.WriteFile("/tmp/42/pic.image", []byte("some image data"), 0600)) + require.NoError(t, os.MkdirAll("/tmp/ava-remark42/42", 0700)) + require.NoError(t, ioutil.WriteFile("/tmp/ava-remark42/42/pic.image", []byte("some image data"), 0600)) tkn, err := srv.Authenticator.TokenService().Token(claims) assert.Nil(t, err) @@ -541,9 +537,9 @@ func TestAdmin_DeleteMeRequest(t *testing.T) { client := http.Client{} req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/admin/deleteme?token=%s", ts.URL, tkn), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") resp, err := client.Do(req) - assert.Nil(t, err) + require.Nil(t, err) assert.Equal(t, 200, resp.StatusCode) _, err = srv.DataService.User("radio-t", "user1", 0, 0) @@ -551,9 +547,8 @@ func TestAdmin_DeleteMeRequest(t *testing.T) { } func TestAdmin_DeleteMeRequestFailed(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}, User: store.User{Name: "user1 name", ID: "user1"}} @@ -569,7 +564,7 @@ func TestAdmin_DeleteMeRequestFailed(t *testing.T) { client := http.Client{} req, err := http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/admin/deleteme?token=%s", ts.URL, "bad token"), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") resp, err := client.Do(req) assert.Nil(t, err) assert.Equal(t, 400, resp.StatusCode) @@ -596,7 +591,7 @@ func TestAdmin_DeleteMeRequestFailed(t *testing.T) { assert.Nil(t, err) req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/admin/deleteme?token=%s", ts.URL, tkn), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "bad-password") + req.SetBasicAuth("admin", "bad-password") resp, err = client.Do(req) assert.Nil(t, err) assert.Equal(t, 401, resp.StatusCode) @@ -608,7 +603,7 @@ func TestAdmin_DeleteMeRequestFailed(t *testing.T) { assert.Nil(t, err) req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/admin/deleteme?token=%s", ts.URL, tkn), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") resp, err = client.Do(req) assert.Nil(t, err) assert.Equal(t, 400, resp.StatusCode, resp.Status) @@ -620,7 +615,7 @@ func TestAdmin_DeleteMeRequestFailed(t *testing.T) { assert.Nil(t, err) req, err = http.NewRequest(http.MethodGet, fmt.Sprintf("%s/api/v1/admin/deleteme?token=%s", ts.URL, tkn), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") resp, err = client.Do(req) assert.Nil(t, err) assert.Equal(t, 403, resp.StatusCode) @@ -630,9 +625,8 @@ func TestAdmin_DeleteMeRequestFailed(t *testing.T) { } func TestAdmin_GetUserInfo(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}, User: store.User{Name: "user1 name", ID: "user1"}} @@ -644,7 +638,7 @@ func TestAdmin_GetUserInfo(t *testing.T) { _, err = srv.DataService.Create(c2) assert.Nil(t, err) - body, code := getWithAuth(t, fmt.Sprintf("%s/api/v1/admin/user/user1?site=radio-t&url=https://radio-t.com/blah", ts.URL)) + body, code := getWithAdminAuth(t, fmt.Sprintf("%s/api/v1/admin/user/user1?site=radio-t&url=https://radio-t.com/blah", ts.URL)) assert.Equal(t, 200, code) u := store.User{} err = json.Unmarshal([]byte(body), &u) @@ -655,6 +649,6 @@ func TestAdmin_GetUserInfo(t *testing.T) { _, code = get(t, fmt.Sprintf("%s/api/v1/admin/user/user1?site=radio-t&url=https://radio-t.com/blah", ts.URL)) assert.Equal(t, 401, code, "no auth") - _, code = getWithAuth(t, fmt.Sprintf("%s/api/v1/admin/user/userX?site=radio-t&url=https://radio-t.com/blah", ts.URL)) + _, code = getWithAdminAuth(t, fmt.Sprintf("%s/api/v1/admin/user/userX?site=radio-t&url=https://radio-t.com/blah", ts.URL)) assert.Equal(t, 400, code, "no info about user") } diff --git a/backend/app/rest/api/migrator_test.go b/backend/app/rest/api/migrator_test.go index 9d856b6d..8ca343e3 100644 --- a/backend/app/rest/api/migrator_test.go +++ b/backend/app/rest/api/migrator_test.go @@ -5,7 +5,6 @@ import ( "compress/gzip" "encoding/json" "fmt" - "github.com/go-pkgz/auth/token" "io" "io/ioutil" "mime/multipart" @@ -16,6 +15,8 @@ import ( "testing" "time" + "github.com/go-pkgz/auth/token" + bolt "github.com/coreos/bbolt" "github.com/go-chi/chi" "github.com/go-pkgz/auth" @@ -39,7 +40,8 @@ func TestMigrator_Import(t *testing.T) { {"id":"83fd97fd-ff64-48d1-9fb7-ca7769c77037","pid":"p1","text":"

test test #2

","user":{"name":"developer one","id":"dev","picture":"/api/v1/avatar/remark.image","profile":"https://remark42.com","admin":true,"ip":"ae12fe3b5f129b5cc4cdd2b136b7b7947c4d2741"},"locator":{"site":"radio-t","url":"https://radio-t.com/blah2"},"score":0,"votes":{},"time":"2018-04-30T01:37:00.861387771-05:00"}`) client := &http.Client{Timeout: 1 * time.Second} - req, err := http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=native&secret=123456", r) + req, err := http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=native", r) + req.SetBasicAuth("admin", "password") assert.Nil(t, err) resp, err := client.Do(req) assert.Nil(t, err) @@ -51,7 +53,7 @@ func TestMigrator_Import(t *testing.T) { client = &http.Client{Timeout: 10 * time.Second} req, err = http.NewRequest("GET", ts.URL+"/import/wait?site=radio-t", nil) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") assert.NoError(t, err) resp, err = client.Do(req) assert.Equal(t, 200, resp.StatusCode) @@ -74,7 +76,8 @@ func TestMigrator_ImportForm(t *testing.T) { contentType := bodyWriter.FormDataContentType() require.NoError(t, bodyWriter.Close()) - resp, err := http.Post(ts.URL+"/import/form?site=radio-t&provider=native&secret=123456", contentType, bodyBuf) + authts := strings.Replace(ts.URL, "http://", "http://admin:password@", 1) + resp, err := http.Post(authts+"/import/form?site=radio-t&provider=native", contentType, bodyBuf) assert.Nil(t, err) assert.Equal(t, http.StatusAccepted, resp.StatusCode) @@ -84,7 +87,7 @@ func TestMigrator_ImportForm(t *testing.T) { client := &http.Client{Timeout: 10 * time.Second} req, err := http.NewRequest("GET", ts.URL+"/import/wait?site=radio-t", nil) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") assert.NoError(t, err) resp, err = client.Do(req) assert.Equal(t, 200, resp.StatusCode) @@ -97,9 +100,10 @@ func TestMigrator_ImportFromWP(t *testing.T) { r := strings.NewReader(strings.Replace(xmlTestWP, "'", "`", -1)) client := &http.Client{Timeout: 1 * time.Second} - req, err := http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=wordpress&secret=123456", r) + req, err := http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=wordpress", r) assert.Nil(t, err) req.Header.Add("Content-Type", "application/xml; charset=utf-8") + req.SetBasicAuth("admin", "password") resp, err := client.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusAccepted, resp.StatusCode) @@ -110,16 +114,15 @@ func TestMigrator_ImportFromWP(t *testing.T) { client = &http.Client{Timeout: 10 * time.Second} req, err = http.NewRequest("GET", ts.URL+"/import/wait?site=radio-t", nil) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") assert.NoError(t, err) resp, err = client.Do(req) assert.Equal(t, 200, resp.StatusCode) assert.NoError(t, ds.Interface.Close()) - srvAccess, tsAccess := prep(t) - require.NotNil(t, srvAccess) - defer cleanup(ts, srvAccess) + tsAccess, _, teardownAccess := startupT(t) + defer teardownAccess() res, code := get(t, tsAccess.URL+"/api/v1/last/10?site=radio-t") require.Equal(t, 200, code) @@ -160,14 +163,16 @@ func TestMigrator_ImportDouble(t *testing.T) { } r := strings.NewReader(`{"version":1}` + strings.Join(recs, "\n")) // reader with 10k records client := &http.Client{Timeout: 1 * time.Second} - req, err := http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=native&secret=123456", r) + req, err := http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=native", r) + req.SetBasicAuth("admin", "password") assert.Nil(t, err) resp, err := client.Do(req) assert.Nil(t, err) assert.Equal(t, http.StatusAccepted, resp.StatusCode) client = &http.Client{Timeout: 1 * time.Second} - req, err = http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=native&secret=123456", r) + req, err = http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=native", r) + req.SetBasicAuth("admin", "password") assert.Nil(t, err) resp, err = client.Do(req) assert.Nil(t, err) @@ -187,7 +192,8 @@ func TestMigrator_ImportWaitExpired(t *testing.T) { } r := strings.NewReader(`{"version":1}` + strings.Join(recs, "\n")) // reader with 10k records client := &http.Client{Timeout: 1 * time.Second} - req, err := http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=native&secret=123456", r) + req, err := http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=native", r) + req.SetBasicAuth("admin", "password") require.Nil(t, err) resp, err := client.Do(req) assert.Nil(t, err) @@ -195,7 +201,7 @@ func TestMigrator_ImportWaitExpired(t *testing.T) { client = &http.Client{Timeout: 10 * time.Second} req, err = http.NewRequest("GET", ts.URL+"/import/wait?site=radio-t&timeout=100ms", nil) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") assert.NoError(t, err) resp, err = client.Do(req) assert.Equal(t, http.StatusGatewayTimeout, resp.StatusCode) @@ -211,21 +217,23 @@ func TestMigrator_Export(t *testing.T) { // import comments first client := &http.Client{Timeout: 1 * time.Second} - req, err := http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=native&secret=123456", r) + req, err := http.NewRequest("POST", ts.URL+"/import?site=radio-t&provider=native", r) require.Nil(t, err) + req.SetBasicAuth("admin", "password") resp, err := client.Do(req) require.Nil(t, err) require.Equal(t, http.StatusAccepted, resp.StatusCode) client = &http.Client{Timeout: 10 * time.Second} req, err = http.NewRequest("GET", ts.URL+"/import/wait?site=radio-t", nil) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") assert.NoError(t, err) resp, err = client.Do(req) assert.Equal(t, 200, resp.StatusCode) // check file mode - req, err = http.NewRequest("GET", ts.URL+"/export?mode=file&site=radio-t&secret=123456", nil) + req, err = http.NewRequest("GET", ts.URL+"/export?mode=file&site=radio-t", nil) require.Nil(t, err) + req.SetBasicAuth("admin", "password") resp, err = client.Do(req) require.Nil(t, err) require.Equal(t, 200, resp.StatusCode) @@ -240,8 +248,9 @@ func TestMigrator_Export(t *testing.T) { t.Logf("%s", string(ungzBody)) // check stream mode - req, err = http.NewRequest("GET", ts.URL+"/export?mode=stream&site=radio-t&secret=123456", nil) + req, err = http.NewRequest("GET", ts.URL+"/export?mode=stream&site=radio-t", nil) require.Nil(t, err) + req.SetBasicAuth("admin", "password") resp, err = client.Do(req) require.Nil(t, err) require.Equal(t, 200, resp.StatusCode) @@ -253,7 +262,7 @@ func TestMigrator_Export(t *testing.T) { assert.Equal(t, 2, strings.Count(string(body), "\"text\"")) t.Logf("%s", string(body)) - req, err = http.NewRequest("GET", ts.URL+"/export?site=radio-t&secret=bad", nil) + req, err = http.NewRequest("GET", ts.URL+"/export?site=radio-t", nil) require.Nil(t, err) resp, err = client.Do(req) require.Nil(t, err) @@ -275,7 +284,7 @@ func prepImportSrv(t *testing.T) (svc *Migrator, ds *service.DataStore, ts *http } a := auth.NewService(auth.Opts{ - DevPasswd: "password", + AdminPasswd: "password", SecretReader: token.SecretFunc(func(id string) (string, error) { return "123456", nil }), Issuer: "test", }) diff --git a/backend/app/rest/api/rest_private_test.go b/backend/app/rest/api/rest_private_test.go index 8c4a4c7f..fcf201d0 100644 --- a/backend/app/rest/api/rest_private_test.go +++ b/backend/app/rest/api/rest_private_test.go @@ -18,9 +18,8 @@ import ( ) func TestRest_Create(t *testing.T) { - srv, ts := prep(t) - require.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() resp, err := post(t, ts.URL+"/api/v1/comment", `{"text": "test 123", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`) @@ -29,6 +28,7 @@ func TestRest_Create(t *testing.T) { assert.Nil(t, err) require.Equal(t, http.StatusCreated, resp.StatusCode, string(b)) + t.Log(string(b)) c := R.JSON{} err = json.Unmarshal(b, &c) assert.Nil(t, err) @@ -39,9 +39,8 @@ func TestRest_Create(t *testing.T) { } func TestRest_CreateOldPost(t *testing.T) { - srv, ts := prep(t) - require.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() // make old, but not too old comment old := store.Comment{Text: "test test old", ParentID: "", Timestamp: time.Now().AddDate(0, 0, -5), @@ -73,9 +72,8 @@ func TestRest_CreateOldPost(t *testing.T) { } func TestRest_CreateTooBig(t *testing.T) { - srv, ts := prep(t) - require.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() longComment := fmt.Sprintf(`{"text": "%4001s", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`, "Щ") @@ -105,9 +103,8 @@ func TestRest_CreateTooBig(t *testing.T) { func TestRest_CreateRejected(t *testing.T) { - srv, ts := prep(t) - require.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() body := `{"text": "test 123", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}` // try to create without auth @@ -117,9 +114,8 @@ func TestRest_CreateRejected(t *testing.T) { } func TestRest_CreateAndGet(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() // create comment resp, err := post(t, ts.URL+"/api/v1/comment", @@ -134,24 +130,31 @@ func TestRest_CreateAndGet(t *testing.T) { id := c["id"].(string) - // get created comment by id - res, code := getWithAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah1", ts.URL, id)) + // get created comment by id as admin + res, code := getWithAdminAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah1", ts.URL, id)) assert.Equal(t, 200, code) comment := store.Comment{} err = json.Unmarshal([]byte(res), &comment) assert.Nil(t, err) assert.Equal(t, "

test 123

\n\n

http://radio-t.com

\n", comment.Text) assert.Equal(t, "**test** *123*\n\n http://radio-t.com", comment.Orig) - assert.Equal(t, store.User{Name: "developer one", ID: "dev", Admin: true, Blocked: false, + assert.Equal(t, store.User{Name: "admin", ID: "admin", Admin: true, Blocked: false, IP: "dbc7c999343f003f189f70aaf52cc04443f90790"}, comment.User) t.Logf("%+v", comment) + + // get created comment by id as non-admin + res, code = getWithDevAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah1", ts.URL, id)) + assert.Equal(t, 200, code) + comment = store.Comment{} + err = json.Unmarshal([]byte(res), &comment) + assert.Nil(t, err) + assert.Equal(t, store.User{Name: "admin", ID: "admin", Admin: true, Blocked: false, IP: ""}, comment.User, "no ip") } func TestRest_Update(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", ParentID: "p1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} @@ -161,7 +164,7 @@ func TestRest_Update(t *testing.T) { req, err := http.NewRequest(http.MethodPut, ts.URL+"/api/v1/comment/"+id+"?site=radio-t&url=https://radio-t.com/blah1", strings.NewReader(`{"text":"updated text", "summary":"my edit"}`)) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.Header.Add("X-JWT", devToken) b, err := client.Do(req) assert.Nil(t, err) body, err := ioutil.ReadAll(b.Body) @@ -179,7 +182,7 @@ func TestRest_Update(t *testing.T) { assert.True(t, time.Since(c2.Edit.Timestamp) < 1*time.Second) // read updated comment - res, code := getWithAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah1", ts.URL, id)) + res, code := getWithAdminAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah1", ts.URL, id)) assert.Equal(t, 200, code) c3 := store.Comment{} err = json.Unmarshal([]byte(res), &c3) @@ -188,9 +191,8 @@ func TestRest_Update(t *testing.T) { } func TestRest_UpdateDelete(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", ParentID: "p1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} @@ -200,7 +202,7 @@ func TestRest_UpdateDelete(t *testing.T) { req, err := http.NewRequest(http.MethodPut, ts.URL+"/api/v1/comment/"+id+"?site=radio-t&url=https://radio-t.com/blah1", strings.NewReader(`{"delete": true, "summary":"removed by user"}`)) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.Header.Add("X-JWT", devToken) b, err := client.Do(req) assert.Nil(t, err) body, err := ioutil.ReadAll(b.Body) @@ -215,7 +217,7 @@ func TestRest_UpdateDelete(t *testing.T) { assert.True(t, c2.Deleted) // read updated comment - res, code := getWithAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah1", ts.URL, id)) + res, code := getWithDevAuth(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah1", ts.URL, id)) assert.Equal(t, 200, code) c3 := store.Comment{} err = json.Unmarshal([]byte(res), &c3) @@ -223,13 +225,11 @@ func TestRest_UpdateDelete(t *testing.T) { assert.Equal(t, "", c3.Text) assert.Equal(t, "", c3.Orig) assert.True(t, c3.Deleted) - } func TestRest_UpdateNotOwner(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", ParentID: "p1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}, User: store.User{ID: "xyz"}} @@ -240,7 +240,7 @@ func TestRest_UpdateNotOwner(t *testing.T) { req, err := http.NewRequest(http.MethodPut, ts.URL+"/api/v1/comment/"+id1+ "?site=radio-t&url=https://radio-t.com/blah1", strings.NewReader(`{"text":"updated text", "summary":"my edit"}`)) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.Header.Add("X-JWT", devToken) b, err := client.Do(req) assert.Nil(t, err) body, err := ioutil.ReadAll(b.Body) @@ -252,16 +252,15 @@ func TestRest_UpdateNotOwner(t *testing.T) { req, err = http.NewRequest(http.MethodPut, ts.URL+"/api/v1/comment/"+id1+ "?site=radio-t&url=https://radio-t.com/blah1", strings.NewReader(`ERRR "text":"updated text", "summary":"my"}`)) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.Header.Add("X-JWT", devToken) b, err = client.Do(req) assert.Nil(t, err) assert.Equal(t, 400, b.StatusCode, string(body), "update is not json") } func TestRest_Vote(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah"}} @@ -276,7 +275,7 @@ func TestRest_Vote(t *testing.T) { req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/api/v1/vote/%s?site=radio-t&url=https://radio-t.com/blah&vote=%d", ts.URL, id1, val), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") resp, err := client.Do(req) assert.Nil(t, err) return resp.StatusCode @@ -290,7 +289,7 @@ func TestRest_Vote(t *testing.T) { err := json.Unmarshal([]byte(body), &cr) assert.Nil(t, err) assert.Equal(t, 1, cr.Score) - assert.Equal(t, map[string]bool{"dev": true}, cr.Votes) + assert.Equal(t, map[string]bool{"admin": true}, cr.Votes) assert.Equal(t, 200, vote(-1), "opposite vote allowed") body, code = get(t, fmt.Sprintf("%s/api/v1/id/%s?site=radio-t&url=https://radio-t.com/blah", ts.URL, id1)) @@ -303,9 +302,8 @@ func TestRest_Vote(t *testing.T) { } func TestRest_UserAllData(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() // write 3 comments user := store.User{ID: "dev", Name: "user name 1"} @@ -325,7 +323,7 @@ func TestRest_UserAllData(t *testing.T) { client := &http.Client{Timeout: 1 * time.Second} req, err := http.NewRequest("GET", ts.URL+"/api/v1/userdata?site=radio-t", nil) require.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.Header.Add("X-JWT", devToken) resp, err := client.Do(req) require.Nil(t, err) require.Equal(t, 200, resp.StatusCode) @@ -336,7 +334,7 @@ func TestRest_UserAllData(t *testing.T) { ungzBody, err := ioutil.ReadAll(ungzReader) assert.NoError(t, err) assert.True(t, strings.HasPrefix(string(ungzBody), - `{"info": {"name":"developer one","id":"dev","picture":"","admin":true}, "comments":[{`)) + `{"info": {"name":"developer one","id":"dev","picture":"http://example.com/pic.png","ip":"127.0.0.1","admin":false}, "comments":[{`)) assert.Equal(t, 3, strings.Count(string(ungzBody), `"text":`), "3 comments inside") t.Logf("%s", string(ungzBody)) @@ -347,7 +345,8 @@ func TestRest_UserAllData(t *testing.T) { err = json.Unmarshal(ungzBody, &parsed) assert.Nil(t, err) - assert.Equal(t, store.User{Name: "developer one", ID: "dev", Picture: "", Admin: true}, parsed.Info) + assert.Equal(t, store.User{Name: "developer one", ID: "dev", + Picture: "http://example.com/pic.png", IP: "127.0.0.1"}, parsed.Info) assert.Equal(t, 3, len(parsed.Comments)) req, err = http.NewRequest("GET", ts.URL+"/api/v1/userdata?site=radio-t", nil) @@ -358,9 +357,8 @@ func TestRest_UserAllData(t *testing.T) { } func TestRest_UserAllDataManyComments(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() user := store.User{ID: "dev", Name: "user name 1"} c := store.Comment{User: user, Text: "test test #1", Locator: store.Locator{SiteID: "radio-t", @@ -376,7 +374,7 @@ func TestRest_UserAllDataManyComments(t *testing.T) { client := &http.Client{Timeout: 1 * time.Second} req, err := http.NewRequest("GET", ts.URL+"/api/v1/userdata?site=radio-t", nil) require.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.Header.Add("X-JWT", devToken) resp, err := client.Do(req) require.Nil(t, err) require.Equal(t, 200, resp.StatusCode) @@ -387,19 +385,18 @@ func TestRest_UserAllDataManyComments(t *testing.T) { ungzBody, err := ioutil.ReadAll(ungzReader) assert.NoError(t, err) assert.True(t, strings.HasPrefix(string(ungzBody), - `{"info": {"name":"developer one","id":"dev","picture":"","admin":true}, "comments":[{`)) + `{"info": {"name":"developer one","id":"dev","picture":"http://example.com/pic.png","ip":"127.0.0.1","admin":false}, "comments":[{`)) assert.Equal(t, 478, strings.Count(string(ungzBody), `"text":`), "478 comments inside") } func TestRest_DeleteMe(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() client := http.Client{} req, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/api/v1/deleteme?site=radio-t", ts.URL), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.Header.Add("X-JWT", devToken) resp, err := client.Do(req) assert.Nil(t, err) assert.Equal(t, 200, resp.StatusCode) diff --git a/backend/app/rest/api/rest_public_test.go b/backend/app/rest/api/rest_public_test.go index 9f40a7f3..48760bdc 100644 --- a/backend/app/rest/api/rest_public_test.go +++ b/backend/app/rest/api/rest_public_test.go @@ -18,9 +18,8 @@ import ( ) func TestRest_Ping(t *testing.T) { - srv, ts := prep(t) - require.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() res, code := get(t, ts.URL+"/api/v1/ping") assert.Equal(t, "pong", res) @@ -28,9 +27,8 @@ func TestRest_Ping(t *testing.T) { } func TestRest_Preview(t *testing.T) { - srv, ts := prep(t) - require.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() resp, err := post(t, ts.URL+"/api/v1/preview", `{"text": "test 123", "locator":{"url": "https://radio-t.com/blah1", "site": "radio-t"}}`) assert.Nil(t, err) @@ -41,9 +39,8 @@ func TestRest_Preview(t *testing.T) { } func TestRest_PreviewWithMD(t *testing.T) { - srv, ts := prep(t) - require.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() text := ` # h1 @@ -69,9 +66,8 @@ BKT } func TestRest_Find(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() _, code := get(t, ts.URL+"/api/v1/find?site=radio-t&url=https://radio-t.com/blah1") assert.Equal(t, 400, code, "nothing in") @@ -123,9 +119,8 @@ func TestRest_Find(t *testing.T) { } func TestRest_FindAge(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", ParentID: "", Timestamp: time.Now().AddDate(0, 0, -5), Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}, User: store.User{ID: "u1"}} @@ -155,9 +150,8 @@ func TestRest_FindAge(t *testing.T) { } func TestRest_FindReadOnly(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", ParentID: "", Timestamp: time.Now().AddDate(0, 0, -1), Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}, User: store.User{ID: "u1"}} @@ -175,7 +169,7 @@ func TestRest_FindReadOnly(t *testing.T) { req, err := http.NewRequest(http.MethodPut, fmt.Sprintf("%s/api/v1/admin/readonly?site=radio-t&url=https://radio-t.com/blah1&ro=1", ts.URL), nil) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") _, err = client.Do(req) require.Nil(t, err) @@ -197,9 +191,8 @@ func TestRest_FindReadOnly(t *testing.T) { } func TestRest_Last(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", ParentID: "p1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} @@ -243,9 +236,8 @@ func TestRest_Last(t *testing.T) { } func TestRest_FindUserComments(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} @@ -280,22 +272,20 @@ func TestRest_FindUserComments(t *testing.T) { } func TestRest_UserInfo(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() - body, code := getWithAuth(t, ts.URL+"/api/v1/user?site=radio-t") + body, code := getWithDevAuth(t, ts.URL+"/api/v1/user?site=radio-t") assert.Equal(t, 200, code) user := store.User{} err := json.Unmarshal([]byte(body), &user) assert.Nil(t, err) - assert.Equal(t, store.User{Name: "developer one", ID: "dev", Picture: "", Admin: true, Blocked: false, IP: ""}, user) + assert.Equal(t, store.User{Name: "developer one", ID: "dev", Picture: "http://example.com/pic.png", IP: "127.0.0.1"}, user) } func TestRest_Count(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} @@ -323,9 +313,8 @@ func TestRest_Count(t *testing.T) { } func TestRest_Counts(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} @@ -353,9 +342,8 @@ func TestRest_Counts(t *testing.T) { } func TestRest_List(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() c1 := store.Comment{Text: "test test #1", Locator: store.Locator{SiteID: "radio-t", URL: "https://radio-t.com/blah1"}} @@ -380,9 +368,8 @@ func TestRest_List(t *testing.T) { } func TestRest_Config(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() body, code := get(t, ts.URL+"/api/v1/config?site=radio-t") assert.Equal(t, 200, code) @@ -400,9 +387,8 @@ func TestRest_Config(t *testing.T) { } func TestRest_Info(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() srv.ReadOnlyAge = 10000000 // make sure we don't hit read-only @@ -438,9 +424,8 @@ func TestRest_Info(t *testing.T) { } func TestRest_Robots(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() body, code := get(t, ts.URL+"/robots.txt") assert.Equal(t, 200, code) diff --git a/backend/app/rest/api/rest_test.go b/backend/app/rest/api/rest_test.go index 11143548..de740492 100644 --- a/backend/app/rest/api/rest_test.go +++ b/backend/app/rest/api/rest_test.go @@ -33,10 +33,11 @@ var testDb = "/tmp/test-remark.db" var testHTML = "/tmp/test-remark.html" var getStartedHTML = "/tmp/getstarted.html" +var devToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJyZW1hcms0MiIsImV4cCI6Mzc4OTE5MTgyMiwianRpIjoicmFuZG9tIGlkIiwiaXNzIjoicmVtYXJrNDIiLCJuYmYiOjE1MjE4ODQyMjIsInVzZXIiOnsibmFtZSI6ImRldmVsb3BlciBvbmUiLCJpZCI6ImRldiIsInBpY3R1cmUiOiJodHRwOi8vZXhhbXBsZS5jb20vcGljLnBuZyIsImlwIjoiMTI3LjAuMC4xIiwiZW1haWwiOiJtZUBleGFtcGxlLmNvbSJ9fQ.aKUAXiZxXypgV7m1wEOgUcyPOvUDXHDi3A06YWKbcLg" + func TestRest_FileServer(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() body, code := get(t, ts.URL+"/web/test-remark.html") assert.Equal(t, 200, code) @@ -44,9 +45,8 @@ func TestRest_FileServer(t *testing.T) { } func TestRest_GetStarted(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() err := ioutil.WriteFile(getStartedHTML, []byte("some html blah"), 0700) assert.Nil(t, err) @@ -175,7 +175,7 @@ func TestRest_RunAutocertModeHTTPOnly(t *testing.T) { srv.Shutdown() } -func prep(t *testing.T) (srv *Rest, ts *httptest.Server) { +func startupT(t *testing.T) (ts *httptest.Server, srv *Rest, teardown func()) { b, err := engine.NewBoltDB(bolt.Options{}, engine.BoltSite{FileName: testDb, SiteID: "radio-t"}) require.Nil(t, err) @@ -192,10 +192,9 @@ func prep(t *testing.T) (srv *Rest, ts *httptest.Server) { srv = &Rest{ DataService: dataStore, Authenticator: auth.NewService(auth.Opts{ - DevPasswd: "password", - SecretReader: token.SecretFunc(func(id string) (string, error) { return "secret", nil }), - AvatarStore: avatar.NewLocalFS("/tmp"), - AvatarResizeLimit: 300, + AdminPasswd: "password", + SecretReader: token.SecretFunc(func(id string) (string, error) { return "secret", nil }), + AvatarStore: avatar.NewLocalFS("/tmp/ava-remark42"), }), Cache: &cache.Nop{}, WebRoot: "/tmp", @@ -217,8 +216,18 @@ func prep(t *testing.T) (srv *Rest, ts *httptest.Server) { err = ioutil.WriteFile(testHTML, []byte("some html"), 0700) assert.Nil(t, err) + ts = httptest.NewServer(srv.routes()) - return srv, ts + + teardown = func() { + ts.Close() + srv.DataService.Close() + os.Remove(testDb) + os.Remove(testHTML) + os.RemoveAll("/tmp/ava-remark42") + } + + return ts, srv, teardown } func get(t *testing.T, url string) (string, int) { @@ -230,11 +239,24 @@ func get(t *testing.T, url string) (string, int) { return string(body), r.StatusCode } -func getWithAuth(t *testing.T, url string) (string, int) { +func getWithDevAuth(t *testing.T, url string) (body string, code int) { client := &http.Client{Timeout: 5 * time.Second} req, err := http.NewRequest("GET", url, nil) require.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.Header.Add("X-JWT", devToken) + r, err := client.Do(req) + require.Nil(t, err) + defer r.Body.Close() + b, err := ioutil.ReadAll(r.Body) + assert.Nil(t, err) + return string(b), r.StatusCode +} + +func getWithAdminAuth(t *testing.T, url string) (string, int) { + client := &http.Client{Timeout: 5 * time.Second} + req, err := http.NewRequest("GET", url, nil) + require.Nil(t, err) + req.SetBasicAuth("admin", "password") r, err := client.Do(req) require.Nil(t, err) defer r.Body.Close() @@ -242,24 +264,22 @@ func getWithAuth(t *testing.T, url string) (string, int) { assert.Nil(t, err) return string(body), r.StatusCode } - func post(t *testing.T, url string, body string) (*http.Response, error) { client := &http.Client{Timeout: 5 * time.Second} req, err := http.NewRequest("POST", url, strings.NewReader(body)) assert.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.SetBasicAuth("admin", "password") return client.Do(req) } func addComment(t *testing.T, c store.Comment, ts *httptest.Server) string { - b, err := json.Marshal(c) require.Nil(t, err, "can't marshal comment %+v", c) client := &http.Client{Timeout: 5 * time.Second} req, err := http.NewRequest("POST", ts.URL+"/api/v1/comment", bytes.NewBuffer(b)) require.Nil(t, err) - req.SetBasicAuth("dev", "password") + req.Header.Add("X-JWT", devToken) resp, err := client.Do(req) require.Nil(t, err) require.Equal(t, http.StatusCreated, resp.StatusCode) diff --git a/backend/app/rest/api/rss_test.go b/backend/app/rest/api/rss_test.go index 3eaffe14..201ac3cd 100644 --- a/backend/app/rest/api/rss_test.go +++ b/backend/app/rest/api/rss_test.go @@ -12,9 +12,8 @@ import ( ) func TestServer_RssPost(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() waitOnSecChange() @@ -53,9 +52,8 @@ func TestServer_RssPost(t *testing.T) { } func TestServer_RssSite(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() waitOnSecChange() @@ -107,9 +105,8 @@ func TestServer_RssSite(t *testing.T) { } func TestServer_RssWithReply(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, _, teardown := startupT(t) + defer teardown() waitOnSecChange() @@ -159,9 +156,8 @@ func TestServer_RssWithReply(t *testing.T) { } func TestServer_RssReplies(t *testing.T) { - srv, ts := prep(t) - assert.NotNil(t, srv) - defer cleanup(ts, srv) + ts, srv, teardown := startupT(t) + defer teardown() waitOnSecChange() diff --git a/backend/vendor/github.com/go-pkgz/auth/.travis.yml b/backend/vendor/github.com/go-pkgz/auth/.travis.yml index db0e0d50..f8747c76 100644 --- a/backend/vendor/github.com/go-pkgz/auth/.travis.yml +++ b/backend/vendor/github.com/go-pkgz/auth/.travis.yml @@ -19,5 +19,5 @@ script: - GO111MODULE=on go get ./... - GO111MODULE=on go mod vendor - GO111MODULE=on go test -v -mod=vendor -covermode=count -coverprofile=profile.cov ./... || travis_terminate 1; - - ./bin/gometalinter --deadline=120s --exclude=test --exclude=mock --exclude=vendor --disable-all --enable=errcheck --enable=vet --enable=vetshadow --enable=megacheck --enable=ineffassign --enable=varcheck --enable=unconvert --enable=deadcode --enable=interfacer --enable=gotype ./... || travis_terminate 1; + - ./bin/gometalinter --deadline=120s --exclude=test --exclude=mock --exclude=vendor --exclude=_example --disable-all --enable=errcheck --enable=vet --enable=vetshadow --enable=megacheck --enable=ineffassign --enable=varcheck --enable=unconvert --enable=deadcode --enable=interfacer --enable=gotype ./... || travis_terminate 1; - $GOPATH/bin/goveralls -coverprofile=profile.cov -service=travis-ci diff --git a/backend/vendor/github.com/go-pkgz/auth/README.md b/backend/vendor/github.com/go-pkgz/auth/README.md index ebb5c881..ccba903f 100644 --- a/backend/vendor/github.com/go-pkgz/auth/README.md +++ b/backend/vendor/github.com/go-pkgz/auth/README.md @@ -1,17 +1,20 @@ -# auth - authentication via oauth2 [![Build Status](https://travis-ci.org/go-pkgz/auth.svg?branch=master)](https://travis-ci.org/go-pkgz/auth) [![Coverage Status](https://coveralls.io/repos/github/go-pkgz/auth/badge.svg?branch=master)](https://coveralls.io/github/go-pkgz/auth?branch=master) +# auth - authentication via oauth2 [![Build Status](https://travis-ci.org/go-pkgz/auth.svg?branch=master)](https://travis-ci.org/go-pkgz/auth) [![Coverage Status](https://coveralls.io/repos/github/go-pkgz/auth/badge.svg?branch=master)](https://coveralls.io/github/go-pkgz/auth?branch=master) [![godoc](https://godoc.org/github.com/go-pkgz/auth?status.svg)](https://godoc.org/github.com/go-pkgz/auth) + + This library provides "social login" with Github, Google, Facebook and Yandex. - Multiple oauth2 providers can be used at the same time - Special `dev` provider allows local testing and development -- JWT stored in a secure cookie and with XSRF protection. Cookies can be session-only +- JWT stored in a secure cookie with XSRF protection. Cookies can be session-only - Minimal scopes with user name, id and picture (avatar) only -- Integrated avatar proxy with FS, boltdb or gridfs storage -- Support of user-defined storages +- Integrated avatar proxy with FS, boltdb and gridfs storages +- Support of user-defined storages for avatars - Black list with user-defined validator - Multiple aud (audience) supported - Secure key with customizable `SecretReader` - Ability to store extra information to token and retrieve on login +- Pre-auth and post-auth hooks to handle custom use cases. - Middleware for easy integration into http routers ## Install @@ -23,25 +26,26 @@ This library provides "social login" with Github, Google, Facebook and Yandex. Example with chi router: ```go + func main() { /// define options options := auth.Opts{ - SecretReader: token.SecretFunc(func(id string) (string, error) { return "secret", nil }), // secret key for JWT + SecretReader: token.SecretFunc(func(id string) (string, error) { // secret key for JWT + return "secret", nil + }), TokenDuration: time.Hour, CookieDuration: time.Hour * 24, Issuer: "my-test-app", URL: "http://127.0.0.1:8080", - AvatarStore: avatar.NewLocalFS("/tmp", 120), - Validator: middleware.ValidatorFunc(func(_ string, claims token.Claims) bool { - return claims.User != nil && strings.HasPrefix(claims.User.Name, "dev_") // allow only dev_ names - }), + AvatarStore: avatar.NewLocalFS("/tmp"), + Validator: token.ValidatorFunc(func(_ string, claims token.Claims) bool { + // allow only dev_* names + return claims.User != nil && strings.HasPrefix(claims.User.Name, "dev_") + }), } - // create auth service - service, err := auth.NewService(options) - if err != nil { - log.Fatal(err) - } + // create auth service with providers + service := auth.NewService(options) service.AddProvider("github", "", "") // add github provider service.AddProvider("facebook", "", "") // add facebook provider @@ -69,7 +73,104 @@ func main() { - `middleware.Auth` - requires authenticated user - `middleware.Admin` - requires authenticated and admin user - `middleware.Trace` - doesn't require authenticated user, but adds user info to request - + +## Details + +Generally, adding support of `auth` includes a few relatively simple steps: + +1. Setup `auth.Opts` structure with all parameters. Each of them [documented](https://github.com/go-pkgz/auth/blob/master/auth.go#L29) and most of parameters are optional and have sane defaults. +2. [Create](https://github.com/go-pkgz/auth/blob/master/auth.go#L56) the new `auth.Service` with provided options. +3. [Add all](https://github.com/go-pkgz/auth/blob/master/auth.go#L149) desirable authentication providers. Currently supported Github, Google, Facebook and Yandex +4. Retrieve [middleware](https://github.com/go-pkgz/auth/blob/master/auth.go#L144) and [http handlers](https://github.com/go-pkgz/auth/blob/master/auth.go#L105) from `auth.Service` +5. Wire auth and avatar handlers into http router as sub–routes. + +### API + +For the example above authentication handlers wired as `/auth` and provides: + +- `/auth//login?id=&from=` - site_id used as `aud` claim for the token and can be processed by `SecretReader` to load/retrieve/define different secrets. redirect_url is the url to redirect after successful login. +- `/avatar/` - returns the avatar (image). Links to those pictures added into user info automatically, for details see "Avatar proxy" +- `/auth//logout` and `/auth/logout` - invalidate "session" by removing JWT cookie +- `/auth/list` - gives a json list of active providers +- `/auth/user` - returns `token.User` (json) + +### User info + +Middleware populates `token.User` to request's context. It can be loaded with `token.GetUserInfo(r *http.Request) (user User, err error)` or `token.MustGetUserInfo(r *http.Request) User` functions. + +`token.User` object includes all fields retrieved from oauth2 provider: +- `Name` - user name +- `ID` - hash of user id +- `Picture` - full link to proxied avatar (see "Avatar proxy") + +It also has placeholders for fields application can populate with custom `token.ClaimsUpdater` (see "Customization") + +- `IP` - hash of user's IP address +- `Email` - user's email +- `Attributes` - map of string:any-value. To simplify management of this map some setters and getters provides, for example `users.StrAttr`, `user.SetBoolAttr` and so on. See [user.go](https://github.com/go-pkgz/auth/blob/master/token/user.go) for more details. + + +### Avatar proxy + +Direct links to avatars won't survive any real-life usage if they linked from a public page. For example, page [like this](https://remark42.com/demo/) may have hundreds of avatars and, most likely, will trigger throttling on provider's side. To eliminate such restriction `auth` library provides and automatic proxy + +- On each login the proxy will retrieve user's picture and save it to `AvatarStore` +- Local (proxied) link to avatar included in user's info (jwt token) +- API for avatar removal provided as a part of `AvatarStore` +- User can leverage one of provided stores: + - `avatar.LocalFS` - file system, each avatar in a separate file + - `avatar.BoltDB` - a single [boltdb](https://github.com/coreos/bbolt) file (embedded KV store). + - `avatar.GridFS` - external [GridFS](https://docs.mongodb.com/manual/core/gridfs/) (mongo db). +- In case of need a custom implementation of other stores can be passed in and used by `auth` library. Each store has to implement `avatar.Store` [interface](https://github.com/go-pkgz/auth/blob/master/avatar/store.go#L25). +- All avatar-related setup done as a part of `auth.Opts` and needs: + - `AvatarStore` - avatar store to use, i.e. `avatar.NewLocalFS("/tmp/avatars")` + - `AvatarRoutePath` - route prefix for direct links to proxied avatar. For example `/api/v1/avatars` will make full links links this - `http://example.com/api/v1/avatars/1234567890123.image`. The url will be stored in user's token and retrieved by middleware (see "User Info") + - `AvatarResizeLimit` - size (in pixel) used to resize avatar. Pls note - resize happens once as a part of `Put` call, i.e. on login. 0 size (default) disables resizing. + +### Customization + +There are several ways to adjust functionality of the library: + +1. `SecretReader` - interface with a single method `Get(aud string) string` to return secret used for JWT signing and verification +1. `ClaimsUpdater` - interface with `Update(claims Claims) Claims` method. This is the primary way to alter a token at login time and add any attributes, set ip, email, admin status and so on. +2. `Validator` - interface with `Validate(token string, claims Claims) bool` method. This is post-token hook and will be called on **each request** wrapped with `Auth` middleware. This will be the place for special logic to reject some tokens or users. + +All of interfaces have corresponding Func wrappers (adapters) - `SecretFunc`, `ClaimsUpdFunc` and `ValidatorFunc`. + +### Implementing black list logic or some other filters + +Restricting some users or some tokens is two step process: + +- `ClaimsUpdater` sets an attribute, like `blocked` (or `allowed`) +- `Validator` checks the attribute and returns true/false + +_This technic used in the [example](https://github.com/go-pkgz/auth/blob/master/_example/backend/main.go#L36) code_ + +The process can be simplified by doing all checks directly in `Validator`, but depends on particular case such solution +can be too expensive because `Validator` runs on each request as a part of auth middleware. In contrast, `ClaimsUpdater` called on token creation/refresh only. + + +### Dev provider + +Working with oauth2 providers can be a pain, especially during development phase. A special, development-only provider `dev` can make it less painful. This one can be registered directly, i.e. `service.AddProvider("dev", "", "")` and should be activated like this: + +```go + // runs dev oauth2 server on :8084 + go func() { + p, err := service.Provider("dev") + if err != nil { + log.Fatal(err) + } + devAuthServer := provider.DevAuthServer{Provider: p} + devAuthServer.Run() + }() +``` + +It will run fake aouth2 "server" on port :8084 and user could login with any user name. See [example](https://github.com/go-pkgz/auth/blob/master/_example/backend/main.go) for more details. + +_Warning: this is not the real oauth2 server but just a small fake thing for development and testing only. Don't use `dev` provider with any production code._ + + ## Register oauth2 providers Authentication handled by external providers. You should setup oauth2 for all (or some) of them to allow users to authenticate. It is not mandatory to have all of them, but at least one should be correctly configured. @@ -77,11 +178,11 @@ Authentication handled by external providers. You should setup oauth2 for all (o #### Google Auth Provider 1. Create a new project: https://console.developers.google.com/project -1. Choose the new project from the top right project dropdown (only if another project is selected) -1. In the project Dashboard center pane, choose **"API Manager"** -1. In the left Nav pane, choose **"Credentials"** -1. In the center pane, choose **"OAuth consent screen"** tab. Fill in **"Product name shown to users"** and hit save. -1. In the center pane, choose **"Credentials"** tab. +2. Choose the new project from the top right project dropdown (only if another project is selected) +3. In the project Dashboard center pane, choose **"API Manager"** +4. In the left Nav pane, choose **"Credentials"** +5. In the center pane, choose **"OAuth consent screen"** tab. Fill in **"Product name shown to users"** and hit save. +6. In the center pane, choose **"Credentials"** tab. * Open the **"New credentials"** drop down * Choose **"OAuth client ID"** * Choose **"Web application"** @@ -89,7 +190,7 @@ Authentication handled by external providers. You should setup oauth2 for all (o * Authorized origins is your domain ex: `https://example.mysite.com` * Authorized redirect URIs is the location of oauth2/callback constructed as domain + `/auth/google/callback`, ex: `https://example.mysite.com/auth/google/callback` * Choose **"Create"** -2. Take note of the **Client ID** and **Client Secret** +7. Take note of the **Client ID** and **Client Secret** _instructions for google oauth2 setup borrowed from [oauth2_proxy](https://github.com/bitly/oauth2_proxy)_ @@ -125,4 +226,6 @@ For more details refer to [Yandex OAuth](https://tech.yandex.com/oauth/doc/dg/co ## Status -The library extracted from [remark42](https://github.com/umputun/remark) project. The code in production use on multiple sites and seems to work fine. \ No newline at end of file +The library extracted from [remark42](https://github.com/umputun/remark) project. The original code in production use on multiple sites and seems to work fine. + +`go-pkgz/auth` library still in beta and until version 1 released some breaking changes still possible. \ No newline at end of file diff --git a/backend/vendor/github.com/go-pkgz/auth/auth.go b/backend/vendor/github.com/go-pkgz/auth/auth.go index a87534e4..d1ef0305 100644 --- a/backend/vendor/github.com/go-pkgz/auth/auth.go +++ b/backend/vendor/github.com/go-pkgz/auth/auth.go @@ -27,7 +27,7 @@ type Service struct { // Opts is a full set of all parameters to initialize Service type Opts struct { - SecretReader token.Secret // reader returns secret for given site id (aud) + SecretReader token.Secret // reader returns secret for given site id (aud), required ClaimsUpd token.ClaimsUpdater // updater for jwt to add/modify values stored in the token SecureCookies bool // makes jwt cookie secure TokenDuration time.Duration // token's TTL, refreshed automatically @@ -42,14 +42,14 @@ type Opts struct { Issuer string // optional value for iss claim, usually the application name, default "go-pkgz/auth" - URL string // root url for the rest service, i.e. http://blah.example.com + URL string // root url for the rest service, i.e. http://blah.example.com, required Validator token.Validator // validator allows to reject some valid tokens with user-defined logic - AvatarStore avatar.Store // store to save/load avatars + AvatarStore avatar.Store // store to save/load avatars, required AvatarResizeLimit int // resize avatar's limit in pixels - AvatarRoutePath string // avatar routing prefix, i.e. "/api/v1/avatar" + AvatarRoutePath string // avatar routing prefix, i.e. "/api/v1/avatar", default `/avatar` - DevPasswd string // if presented, allows basic auth with user dev and given password + AdminPasswd string // if presented, allows basic auth with user admin and given password } // NewService initializes everything @@ -71,7 +71,7 @@ func NewService(opts Opts) *Service { if opts.SecretReader == nil { jwtService.SecretReader = token.SecretFunc(func(id string) (string, error) { - return "", errors.New("secrets reader not avalibale") + return "", errors.New("secrets reader not available") }) } @@ -79,9 +79,9 @@ func NewService(opts Opts) *Service { opts: opts, jwtService: jwtService, authMiddleware: middleware.Authenticator{ - JWTService: jwtService, - Validator: opts.Validator, - DevPasswd: opts.DevPasswd, + JWTService: jwtService, + Validator: opts.Validator, + AdminPasswd: opts.AdminPasswd, }, } @@ -96,6 +96,9 @@ func NewService(opts Opts) *Service { RoutePath: opts.AvatarRoutePath, ResizeLimit: opts.AvatarResizeLimit, } + if res.avatarProxy.RoutePath == "" { + res.avatarProxy.RoutePath = "/avatar" + } } return &res @@ -104,7 +107,7 @@ func NewService(opts Opts) *Service { // Handlers gets http.Handler for all providers and avatars func (s *Service) Handlers() (authHandler http.Handler, avatarHandler http.Handler) { - providerHandler := func(w http.ResponseWriter, r *http.Request) { + ah := func(w http.ResponseWriter, r *http.Request) { elems := strings.Split(r.URL.Path, "/") if len(elems) < 2 { w.WriteHeader(http.StatusBadRequest) @@ -127,6 +130,19 @@ func (s *Service) Handlers() (authHandler http.Handler, avatarHandler http.Handl return } + // show user info + if elems[len(elems)-1] == "user" { + claims, _, err := s.jwtService.Get(r) + if err != nil { + w.WriteHeader(http.StatusUnauthorized) + rest.RenderJSON(w, r, rest.JSON{"error": err.Error()}) + return + } + rest.RenderJSON(w, r, claims.User) + return + } + + // regular auth handlers provName := elems[len(elems)-2] p, err := s.Provider(provName) if err != nil { @@ -137,10 +153,10 @@ func (s *Service) Handlers() (authHandler http.Handler, avatarHandler http.Handl p.Handler(w, r) } - return http.HandlerFunc(providerHandler), http.HandlerFunc(s.avatarProxy.Handler) + return http.HandlerFunc(ah), http.HandlerFunc(s.avatarProxy.Handler) } -// Middleware returns token middleware +// Middleware returns auth middleware func (s *Service) Middleware() middleware.Authenticator { return s.authMiddleware } @@ -152,7 +168,7 @@ func (s *Service) AddProvider(name string, cid string, csecret string) { URL: s.opts.URL, JwtService: s.jwtService, Issuer: s.issuer, - AvatarProxy: s.avatarProxy, + AvatarSaver: s.avatarProxy, Cid: cid, Csecret: csecret, } diff --git a/backend/vendor/github.com/go-pkgz/auth/go.mod b/backend/vendor/github.com/go-pkgz/auth/go.mod index 2369b3bd..736ae648 100644 --- a/backend/vendor/github.com/go-pkgz/auth/go.mod +++ b/backend/vendor/github.com/go-pkgz/auth/go.mod @@ -2,22 +2,14 @@ module github.com/go-pkgz/auth require ( cloud.google.com/go v0.34.0 // indirect - github.com/boltdb/bolt v1.3.1 // indirect github.com/coreos/bbolt v1.3.0 github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 - github.com/go-errors/errors v1.0.1 github.com/go-pkgz/mongo v1.0.0 - github.com/go-pkgz/rest v1.1.1 - github.com/kr/pretty v0.1.0 // indirect + github.com/go-pkgz/rest v1.1.5 github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022 github.com/pkg/errors v0.8.0 - github.com/stretchr/testify v1.2.2 golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b golang.org/x/net v0.0.0-20181220203305-927f97764cc3 // indirect golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890 - golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 // indirect - golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6 // indirect - google.golang.org/appengine v1.4.0 // indirect - gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect ) diff --git a/backend/vendor/github.com/go-pkgz/auth/go.sum b/backend/vendor/github.com/go-pkgz/auth/go.sum index 2676c7cb..63a7900c 100644 --- a/backend/vendor/github.com/go-pkgz/auth/go.sum +++ b/backend/vendor/github.com/go-pkgz/auth/go.sum @@ -1,7 +1,5 @@ cloud.google.com/go v0.34.0 h1:eOI3/cP2VTU6uZLDYAoic+eyzzB9YyGmJ7eIjl8rOPg= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/boltdb/bolt v1.3.1 h1:JQmyP4ZBrce+ZQu0dY660FMfatumYDLun9hBCUVIkF4= -github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps= github.com/coreos/bbolt v1.3.0 h1:HIgH5xUWXT914HCI671AxuTTqjj64UOFr7pHn48LUTI= github.com/coreos/bbolt v1.3.0/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -10,20 +8,15 @@ github.com/dgrijalva/jwt-go v3.2.0+incompatible h1:7qlOGliEKZXTDg6OTjfoBKDXWrumC github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8 h1:DujepqpGd1hyOd7aW59XpK7Qymp8iy83xq74fLr21is= github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0OGD1HRkm4kmhM+pmpv3AKq5SU7GMg4oO/Q= -github.com/go-errors/errors v1.0.1 h1:LUHzmkK3GUKUrL/1gfBUxAHzcev3apQlezX/+O7ma6w= -github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-pkgz/mongo v1.0.0 h1:9jijAK7prCRMetiyTu3c1rv/2lMypzuf2DWcVpTlwzw= github.com/go-pkgz/mongo v1.0.0/go.mod h1:R9si/F2aJsjz4MUxhzuppIHY8yLV3YCeuCpgcI50cu4= -github.com/go-pkgz/rest v1.1.1 h1:YuLe+wOJwcE+Y0SkJ+AtvUOPGjTMe4Q4vg98Uqs9CKc= -github.com/go-pkgz/rest v1.1.1/go.mod h1:DIxxm3vSt6e+IY+UQUOFsfB2YaHLmGoOfPLWN5pxQSA= -github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/go-pkgz/rest v1.1.3 h1:rMf+xJn8i1Ip9OKohusZsRxwntM0BwYu8OX8BuEwN80= +github.com/go-pkgz/rest v1.1.3/go.mod h1:DIxxm3vSt6e+IY+UQUOFsfB2YaHLmGoOfPLWN5pxQSA= +github.com/go-pkgz/rest v1.1.4 h1:/Lrg9kBWBjNah7nmCDHLszRAfVVBIy5ajf0vVgpHPi0= +github.com/go-pkgz/rest v1.1.4/go.mod h1:DIxxm3vSt6e+IY+UQUOFsfB2YaHLmGoOfPLWN5pxQSA= +github.com/go-pkgz/rest v1.1.5 h1:5br4mnscfLb27yxv5hJFLBVmAt09PrmIBP+meA3CfHc= +github.com/go-pkgz/rest v1.1.5/go.mod h1:DIxxm3vSt6e+IY+UQUOFsfB2YaHLmGoOfPLWN5pxQSA= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022 h1:Ys0rDzh8s4UMlGaDa1UTA0sfKgvF0hQZzTYX8ktjiDc= github.com/nullrocks/identicon v0.0.0-20180626043057-7875f45b0022/go.mod h1:x4NsS+uc7ecH/Cbm9xKQ6XzmJM57rWTkjywjfB2yQ18= github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= @@ -34,17 +27,7 @@ github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b h1:VHyIDlv3XkfCa5/a81uzaoDkHH4rr81Z62g+xlnO8uM= golang.org/x/image v0.0.0-20181116024801-cd38e8056d9b/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181220203305-927f97764cc3 h1:eH6Eip3UpmR+yM/qI9Ijluzb1bNv/cAU/n+6l8tRSis= golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890 h1:uESlIz09WIHT2I+pasSXcpLYqYK8wHcdCetU3VuMBJE= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4 h1:YUO/7uOKsKeq9UokNS62b8FYywz3ker1l1vDZRCRefw= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6 h1:IcgEB62HYgAhX0Nd/QrVgZlxlcyxbGQHElLUhW2X4Fo= -golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/backend/vendor/github.com/go-pkgz/auth/middleware/auth.go b/backend/vendor/github.com/go-pkgz/auth/middleware/auth.go index 80537ea6..164a35c0 100644 --- a/backend/vendor/github.com/go-pkgz/auth/middleware/auth.go +++ b/backend/vendor/github.com/go-pkgz/auth/middleware/auth.go @@ -13,20 +13,12 @@ import ( "github.com/go-pkgz/auth/token" ) -// Authenticator is top level token object providing middlewares +// Authenticator is top level auth object providing middlewares type Authenticator struct { - JWTService *token.Service - Providers []provider.Service - Validator token.Validator - DevPasswd string -} - -var devUser = token.User{ - ID: "dev", - Name: "developer one", - Attributes: map[string]interface{}{ - "admin": true, - }, + JWTService *token.Service + Providers []provider.Service + Validator token.Validator + AdminPasswd string } var adminUser = token.User{ @@ -37,7 +29,7 @@ var adminUser = token.User{ }, } -// Auth middleware adds token from session and populates user info +// Auth middleware adds auth from session and populates user info func (a *Authenticator) Auth(next http.Handler) http.Handler { return a.auth(true)(next) } @@ -47,6 +39,7 @@ func (a *Authenticator) Trace(next http.Handler) http.Handler { return a.auth(false)(next) } +// auth implements all logic for authentication (reqAuth=true) and tracing (reqAuth=false) func (a *Authenticator) auth(reqAuth bool) func(http.Handler) http.Handler { onError := func(h http.Handler, w http.ResponseWriter, r *http.Request, err error) { @@ -57,27 +50,20 @@ func (a *Authenticator) auth(reqAuth bool) func(http.Handler) http.Handler { h.ServeHTTP(w, r) return } - log.Printf("[DEBUG] failed token, %s", err) + log.Printf("[DEBUG] auth failed, %s", err) http.Error(w, "Unauthorized", http.StatusUnauthorized) } f := func(h http.Handler) http.Handler { fn := func(w http.ResponseWriter, r *http.Request) { - // if secret key matches for given site (from request) return admin user - if a.checkSecretKey(r) { + // use admin user basic auth if enabled + if a.basicAdminUser(r) { r = token.SetUserInfo(r, adminUser) h.ServeHTTP(w, r) return } - // use dev user basic token if enabled - if a.basicDevUser(r) { - r = token.SetUserInfo(r, devUser) - h.ServeHTTP(w, r) - return - } - claims, tkn, err := a.JWTService.Get(r) if err != nil { onError(h, w, r, errors.Wrap(err, "can't get token")) @@ -85,12 +71,12 @@ func (a *Authenticator) auth(reqAuth bool) func(http.Handler) http.Handler { } if claims.Handshake != nil { // handshake in token indicate special use cases, not for login - onError(h, w, r, errors.Errorf("invalid kind of token for %s/%s", claims.User.Name, claims.User.ID)) + onError(h, w, r, errors.New("invalid kind of token")) return } if claims.User == nil { - onError(h, w, r, errors.New("failed token, no user info presented in the claim")) + onError(h, w, r, errors.New("failed auth, no user info presented in the claim")) return } @@ -121,29 +107,11 @@ func (a *Authenticator) auth(reqAuth bool) func(http.Handler) http.Handler { return f } -func (a *Authenticator) checkSecretKey(r *http.Request) bool { - if a.JWTService.SecretReader == nil { - return false - } - - aud := r.URL.Query().Get("aud") - secret := r.URL.Query().Get("secret") - - skey, err := a.JWTService.SecretReader.Get(aud) - if err != nil { - return false - } - - if strings.TrimSpace(secret) == "" || secret != skey { - return false - } - return true -} - -// refreshExpiredToken makes new token with passed claims, but only if permission allowed +// refreshExpiredToken makes a new token with passed claims func (a *Authenticator) refreshExpiredToken(w http.ResponseWriter, claims token.Claims) (token.Claims, error) { - // refresh token - if err := a.JWTService.Set(w, claims, false); err != nil { + + claims.ExpiresAt = 0 // this will cause now+duration for refreshed token + if err := a.JWTService.Set(w, claims); err != nil { return token.Claims{}, err } return claims, nil @@ -168,9 +136,10 @@ func (a *Authenticator) AdminOnly(next http.Handler) http.Handler { return http.HandlerFunc(fn) } -func (a *Authenticator) basicDevUser(r *http.Request) bool { +// basic auth for admin user +func (a *Authenticator) basicAdminUser(r *http.Request) bool { - if a.DevPasswd == "" { + if a.AdminPasswd == "" { return false } @@ -181,18 +150,18 @@ func (a *Authenticator) basicDevUser(r *http.Request) bool { b, err := base64.StdEncoding.DecodeString(s[1]) if err != nil { - log.Printf("[WARN] dev user token failed, failed to decode %s, %s", s[1], err) + log.Printf("[WARN] admin user auth failed, can't to decode %s, %s", s[1], err) return false } pair := strings.SplitN(string(b), ":", 2) if len(pair) != 2 { - log.Printf("[WARN] dev user token failed, failed to split %s", string(b)) + log.Printf("[WARN] admin user auth failed, can't split basic auth %s", string(b)) return false } - if pair[0] != "dev" || pair[1] != a.DevPasswd { - log.Printf("[WARN] dev user token failed, user/passwd mismatch %+v", pair) + if pair[0] != "admin" || pair[1] != a.AdminPasswd { + log.Printf("[WARN] dev user auth failed, user/passwd mismatch %+v", pair) return false } diff --git a/backend/vendor/github.com/go-pkgz/auth/provider/dev_provider.go b/backend/vendor/github.com/go-pkgz/auth/provider/dev_provider.go index 514a35e9..9dc6226b 100644 --- a/backend/vendor/github.com/go-pkgz/auth/provider/dev_provider.go +++ b/backend/vendor/github.com/go-pkgz/auth/provider/dev_provider.go @@ -8,6 +8,7 @@ import ( "net/http" "strings" "sync" + "text/template" "time" "github.com/nullrocks/identicon" @@ -46,6 +47,12 @@ func (d *DevAuthServer) Run() { log.Printf("[WARN] can't create identicon, %s", err) } + userFormTmpl, err := template.New("page").Parse(devUserFormTmpl) + if err != nil { + log.Printf("[WARN] can't parse user form template, %s", err) + return + } + d.httpServer = &http.Server{ Addr: fmt.Sprintf(":%d", devAuthPort), Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -56,7 +63,10 @@ func (d *DevAuthServer) Run() { // first time it will be called without username and will ask for one if !d.Automatic && (r.ParseForm() != nil || r.Form.Get("username") == "") { - if _, err = w.Write([]byte(fmt.Sprintf(devUserForm, r.URL.RawQuery))); err != nil { + + formData := struct{ Query string }{Query: r.URL.RawQuery} + + if err = userFormTmpl.Execute(w, formData); err != nil { log.Printf("[WARN] can't write, %s", err) } return @@ -176,24 +186,121 @@ func (d *DevAuthServer) genAvatar(user string) ([]byte, error) { return buf.Bytes(), err } -var devUserForm = ` +var devUserFormTmpl = ` - - Dev User - - + + Dev OAuth + + -
- username: - + +
+

GO-PKGZ/AUTH

+

Dev Provider

+
+ + +

Not for production use

- + + ` diff --git a/backend/vendor/github.com/go-pkgz/auth/provider/providers.go b/backend/vendor/github.com/go-pkgz/auth/provider/providers.go index 0241b347..cb5ee7db 100644 --- a/backend/vendor/github.com/go-pkgz/auth/provider/providers.go +++ b/backend/vendor/github.com/go-pkgz/auth/provider/providers.go @@ -18,8 +18,8 @@ func NewGoogle(p Params) Service { return initService(p, Service{ Name: "google", Endpoint: google.Endpoint, - RedirectURL: p.URL + "/token/google/callback", - Scopes: []string{"https://www.googleapis.com/token/userinfo.profile"}, + RedirectURL: p.URL + "/auth/google/callback", + Scopes: []string{"https://www.googleapis.com/auth/userinfo.profile"}, InfoURL: "https://www.googleapis.com/oauth2/v3/userinfo", MapUser: func(data userData, _ []byte) token.User { userInfo := token.User{ @@ -41,7 +41,7 @@ func NewGithub(p Params) Service { return initService(p, Service{ Name: "github", Endpoint: github.Endpoint, - RedirectURL: p.URL + "/token/github/callback", + RedirectURL: p.URL + "/auth/github/callback", Scopes: []string{}, InfoURL: "https://api.github.com/user", MapUser: func(data userData, _ []byte) token.User { @@ -76,7 +76,7 @@ func NewFacebook(p Params) Service { return initService(p, Service{ Name: "facebook", Endpoint: facebook.Endpoint, - RedirectURL: p.URL + "/token/facebook/callback", + RedirectURL: p.URL + "/auth/facebook/callback", Scopes: []string{"public_profile"}, InfoURL: "https://graph.facebook.com/me?fields=id,name,picture", MapUser: func(data userData, bdata []byte) token.User { @@ -102,7 +102,7 @@ func NewYandex(p Params) Service { return initService(p, Service{ Name: "yandex", Endpoint: yandex.Endpoint, - RedirectURL: p.URL + "/token/yandex/callback", + RedirectURL: p.URL + "/auth/yandex/callback", Scopes: []string{}, // See https://tech.yandex.com/passport/doc/dg/reference/response-docpage/ InfoURL: "https://login.yandex.ru/info?format=json", diff --git a/backend/vendor/github.com/go-pkgz/auth/provider/service.go b/backend/vendor/github.com/go-pkgz/auth/provider/service.go index 9d05b828..df98d6a6 100644 --- a/backend/vendor/github.com/go-pkgz/auth/provider/service.go +++ b/backend/vendor/github.com/go-pkgz/auth/provider/service.go @@ -17,7 +17,6 @@ import ( "github.com/pkg/errors" "golang.org/x/oauth2" - "github.com/go-pkgz/auth/avatar" "github.com/go-pkgz/auth/token" ) @@ -37,12 +36,17 @@ type Service struct { type Params struct { URL string JwtService *token.Service - AvatarProxy *avatar.Proxy + AvatarSaver AvatarSaver Cid string Csecret string Issuer string } +// AvatarSaver defines minimal interface to save avatar +type AvatarSaver interface { + Put(u token.User) (avatarURL string, err error) +} + type userData map[string]interface{} func (u userData) value(key string) string { @@ -53,9 +57,9 @@ func (u userData) value(key string) string { return "" } -// initService makes token service for given provider +// initService makes oauth2 service for given provider func initService(p Params, service Service) Service { - log.Printf("[INFO] init token service %s", service.Name) + log.Printf("[INFO] init oauth2 service %s", service.Name) service.Params = p service.conf = oauth2.Config{ ClientID: service.Cid, @@ -65,7 +69,7 @@ func initService(p Params, service Service) Service { Endpoint: service.Endpoint, } - log.Printf("[DEBUG] created %s token, id=%s, redir=%s, endpoint=%s", + log.Printf("[DEBUG] created %s oauth2, id=%s, redir=%s, endpoint=%s", service.Name, service.Cid, service.Endpoint, service.RedirectURL) return service } @@ -123,7 +127,7 @@ func (p Service) loginHandler(w http.ResponseWriter, r *http.Request) { }, } - if err := p.JwtService.Set(w, claims, false); err != nil { + if err := p.JwtService.Set(w, claims); err != nil { rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to set token") return } @@ -144,9 +148,14 @@ func (p Service) authHandler(w http.ResponseWriter, r *http.Request) { return } + if oauthClaims.Handshake == nil { + rest.SendErrorJSON(w, r, http.StatusForbidden, nil, "finvalid handshake token") + return + } + retrievedState := oauthClaims.Handshake.State if retrievedState == "" || retrievedState != r.URL.Query().Get("state") { - http.Error(w, fmt.Sprintf("unexpected state %v", retrievedState), http.StatusUnauthorized) + rest.SendErrorJSON(w, r, http.StatusForbidden, nil, "unexpected state") return } @@ -160,7 +169,7 @@ func (p Service) authHandler(w http.ResponseWriter, r *http.Request) { client := p.conf.Client(context.Background(), tok) uinfo, err := client.Get(p.InfoURL) if err != nil { - rest.SendErrorJSON(w, r, http.StatusBadRequest, err, fmt.Sprintf("failed to get client info via %s", p.InfoURL)) + rest.SendErrorJSON(w, r, http.StatusServiceUnavailable, err, "failed to get client info") return } @@ -201,8 +210,8 @@ func (p Service) authHandler(w http.ResponseWriter, r *http.Request) { SessionOnly: oauthClaims.SessionOnly, } - if err = p.JwtService.Set(w, claims, oauthClaims.SessionOnly); err != nil { - rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to save user info") + if err = p.JwtService.Set(w, claims); err != nil { + rest.SendErrorJSON(w, r, http.StatusInternalServerError, err, "failed to set token") return } @@ -218,8 +227,8 @@ func (p Service) authHandler(w http.ResponseWriter, r *http.Request) { // setAvatar saves avatar and puts proxied URL to u.Picture func (p Service) setAvatar(u token.User) token.User { - if p.AvatarProxy != nil { - if avatarURL, e := p.AvatarProxy.Put(u); e == nil { + if p.AvatarSaver != nil { + if avatarURL, e := p.AvatarSaver.Put(u); e == nil { u.Picture = avatarURL } else { log.Printf("[WARN] failed to set avatar for %+v, %+v", u, e) @@ -231,7 +240,6 @@ func (p Service) setAvatar(u token.User) token.User { // LogoutHandler - GET /logout func (p Service) LogoutHandler(w http.ResponseWriter, r *http.Request) { p.JwtService.Reset(w) - log.Printf("[DEBUG] logout") } func (p Service) randToken() (string, error) { diff --git a/backend/vendor/github.com/go-pkgz/auth/token/jwt.go b/backend/vendor/github.com/go-pkgz/auth/token/jwt.go index 97f5f2bc..96e918b6 100644 --- a/backend/vendor/github.com/go-pkgz/auth/token/jwt.go +++ b/backend/vendor/github.com/go-pkgz/auth/token/jwt.go @@ -154,7 +154,7 @@ func (j *Service) Parse(tokenString string) (Claims, error) { // Set creates token cookie with xsrf cookie and put it to ResponseWriter // accepts claims and sets expiration if none defined. permanent flag means long-living cookie, // false makes it session only. -func (j *Service) Set(w http.ResponseWriter, claims Claims, sessionOnly bool) error { +func (j *Service) Set(w http.ResponseWriter, claims Claims) error { if claims.ExpiresAt == 0 { claims.ExpiresAt = time.Now().Add(j.TokenDuration).Unix() } @@ -167,7 +167,7 @@ func (j *Service) Set(w http.ResponseWriter, claims Claims, sessionOnly bool) er } cookieExpiration := 0 // session cookie - if !sessionOnly { + if !claims.SessionOnly && claims.Handshake == nil { cookieExpiration = int(j.CookieDuration.Seconds()) } diff --git a/backend/vendor/github.com/go-pkgz/rest/README.md b/backend/vendor/github.com/go-pkgz/rest/README.md index 6e80e899..8a6c6f05 100644 --- a/backend/vendor/github.com/go-pkgz/rest/README.md +++ b/backend/vendor/github.com/go-pkgz/rest/README.md @@ -1,4 +1,5 @@ -## REST helpers and middleware [![Build Status](https://travis-ci.org/go-pkgz/rest.svg?branch=master)](https://travis-ci.org/go-pkgz/rest) [![Go Report Card](https://goreportcard.com/badge/github.com/go-pkgz/rest)](https://goreportcard.com/report/github.com/go-pkgz/rest) [![Coverage Status](https://coveralls.io/repos/github/go-pkgz/rest/badge.svg?branch=master)](https://coveralls.io/github/go-pkgz/rest?branch=master) +## REST helpers and middleware [![Build Status](https://travis-ci.org/go-pkgz/rest.svg?branch=master)](https://travis-ci.org/go-pkgz/rest) [![Go Report Card](https://goreportcard.com/badge/github.com/go-pkgz/rest)](https://goreportcard.com/report/github.com/go-pkgz/rest) [![Coverage Status](https://coveralls.io/repos/github/go-pkgz/rest/badge.svg?branch=master)](https://coveralls.io/github/go-pkgz/rest?branch=master) [![godoc](https://godoc.org/github.com/go-pkgz/rest?status.svg)](https://godoc.org/github.com/go-pkgz/rest) + ## Install and update diff --git a/backend/vendor/github.com/go-pkgz/rest/httperrors.go b/backend/vendor/github.com/go-pkgz/rest/httperrors.go index c9730ffc..e5d20588 100644 --- a/backend/vendor/github.com/go-pkgz/rest/httperrors.go +++ b/backend/vendor/github.com/go-pkgz/rest/httperrors.go @@ -1,6 +1,7 @@ package rest import ( + "errors" "fmt" "log" "net/http" @@ -9,14 +10,14 @@ import ( "strings" ) -// SendErrorJSON makes {error: blah, details: blah} json body and responds with error code -func SendErrorJSON(w http.ResponseWriter, r *http.Request, code int, err error, details string) { - log.Printf("[DEBUG] %s", errDetailsMsg(r, code, err, details)) +// SendErrorJSON sends {error: msg} with error code and logging error and caller +func SendErrorJSON(w http.ResponseWriter, r *http.Request, code int, err error, msg string) { + log.Printf("[DEBUG] %s", errDetailsMsg(r, code, err, msg)) w.WriteHeader(code) - RenderJSON(w, r, map[string]interface{}{"error": err.Error(), "details": details}) + RenderJSON(w, r, JSON{"error": msg}) } -func errDetailsMsg(r *http.Request, code int, err error, details string) string { +func errDetailsMsg(r *http.Request, code int, err error, msg string) string { q := r.URL.String() if qun, e := url.QueryUnescape(q); e == nil { @@ -35,5 +36,8 @@ func errDetailsMsg(r *http.Request, code int, err error, details string) string if pos := strings.Index(remoteIP, ":"); pos >= 0 { remoteIP = remoteIP[:pos] } - return fmt.Sprintf("%s - %v - %d - %s - %s%s", details, err, code, remoteIP, q, srcFileInfo) + if err == nil { + err = errors.New("no error") + } + return fmt.Sprintf("%s - %v - %d - %s - %s%s", msg, err, code, remoteIP, q, srcFileInfo) } diff --git a/backend/vendor/github.com/go-pkgz/rest/logger/logger.go b/backend/vendor/github.com/go-pkgz/rest/logger/logger.go index d3746101..2bdbc882 100644 --- a/backend/vendor/github.com/go-pkgz/rest/logger/logger.go +++ b/backend/vendor/github.com/go-pkgz/rest/logger/logger.go @@ -36,6 +36,13 @@ const ( None ) +// Logger returns default logger middleware +func Logger(next http.Handler) http.Handler { + l := New(Flags(All), Prefix("[REST]")) + return l.Handler(next) + +} + // New makes rest Logger with given options func New(options ...Option) *Middleware { res := Middleware{