-
Notifications
You must be signed in to change notification settings - Fork 134
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add backend support for API keys * Add last_used field to API keys * Use secure random value as key secret * Add tests for ListAPIKeys and AddAPIKey * Cover the rest of psql_apikeys.go with tests * Refactor the code a bit * Storing SQL queries in a struct should ensure that `datastore.ModifySQLStatement` gets called on all of them. * A wrapper func around `db.Exec` reduces copypasta. * Actually call `InitRepositoryProvider` for API keys package * Add route handler tests using gomock * Actually use skipper in xsrfMiddleware; minor clean-up
- Loading branch information
1 parent
0ea6632
commit 5eb7f0e
Showing
13 changed files
with
1,219 additions
and
60 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
package main | ||
|
||
import ( | ||
"errors" | ||
"net/http" | ||
|
||
"github.com/labstack/echo" | ||
log "github.com/sirupsen/logrus" | ||
) | ||
|
||
func (p *portalProxy) addAPIKey(c echo.Context) error { | ||
log.Debug("addAPIKey") | ||
|
||
userGUID := c.Get("user_id").(string) | ||
comment := c.FormValue("comment") | ||
|
||
if len(comment) == 0 { | ||
return echo.NewHTTPError(http.StatusBadRequest, "Comment can't be empty") | ||
} | ||
|
||
apiKey, err := p.APIKeysRepository.AddAPIKey(userGUID, comment) | ||
if err != nil { | ||
log.Errorf("Error adding API key: %v", err) | ||
return errors.New("Error adding API key") | ||
} | ||
|
||
return c.JSON(http.StatusOK, apiKey) | ||
} | ||
|
||
func (p *portalProxy) listAPIKeys(c echo.Context) error { | ||
log.Debug("listAPIKeys") | ||
|
||
userGUID := c.Get("user_id").(string) | ||
|
||
apiKeys, err := p.APIKeysRepository.ListAPIKeys(userGUID) | ||
if err != nil { | ||
log.Errorf("Error listing API keys: %v", err) | ||
return errors.New("Error listing API keys") | ||
} | ||
|
||
return c.JSON(http.StatusOK, apiKeys) | ||
} | ||
|
||
func (p *portalProxy) deleteAPIKey(c echo.Context) error { | ||
log.Debug("deleteAPIKey") | ||
|
||
userGUID := c.Get("user_id").(string) | ||
keyGUID := c.FormValue("guid") | ||
|
||
if len(keyGUID) == 0 { | ||
return echo.NewHTTPError(http.StatusBadRequest, "API key guid can't be empty") | ||
} | ||
|
||
if err := p.APIKeysRepository.DeleteAPIKey(userGUID, keyGUID); err != nil { | ||
log.Errorf("Error deleting API key: %v", err) | ||
return errors.New("Error deleting API key") | ||
} | ||
|
||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,289 @@ | ||
package main | ||
|
||
import ( | ||
"encoding/json" | ||
"errors" | ||
"net/http" | ||
"testing" | ||
|
||
"github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/apikeys" | ||
"github.com/cloudfoundry-incubator/stratos/src/jetstream/repository/interfaces" | ||
"github.com/golang/mock/gomock" | ||
"github.com/labstack/echo" | ||
log "github.com/sirupsen/logrus" | ||
. "github.com/smartystreets/goconvey/convey" | ||
) | ||
|
||
func Test_addAPIKey(t *testing.T) { | ||
t.Parallel() | ||
|
||
Convey("Given a request to add an API key", t, func() { | ||
log.SetLevel(log.WarnLevel) | ||
|
||
userID := "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" | ||
|
||
Convey("when comment is not specified", func() { | ||
req := setupMockReq("POST", "", map[string]string{}) | ||
|
||
_, _, ctx, pp, db, _ := setupHTTPTest(req) | ||
defer db.Close() | ||
|
||
ctx.Set("user_id", userID) | ||
|
||
err := pp.addAPIKey(ctx) | ||
|
||
Convey("should return an error", func() { | ||
So(err, ShouldResemble, echo.NewHTTPError(http.StatusBadRequest, "Comment can't be empty")) | ||
}) | ||
}) | ||
|
||
Convey("when a DB error occurs", func() { | ||
comment := "Test API key" | ||
|
||
ctrl := gomock.NewController(t) | ||
defer ctrl.Finish() | ||
|
||
m := apikeys.NewMockRepository(ctrl) | ||
m. | ||
EXPECT(). | ||
AddAPIKey(gomock.Eq(userID), gomock.Eq(comment)). | ||
Return(nil, errors.New("Something went wrong")) | ||
|
||
req := setupMockReq("POST", "", map[string]string{ | ||
"comment": comment, | ||
}) | ||
|
||
_, _, ctx, pp, db, _ := setupHTTPTest(req) | ||
defer db.Close() | ||
|
||
pp.APIKeysRepository = m | ||
|
||
ctx.Set("user_id", userID) | ||
|
||
err := pp.addAPIKey(ctx) | ||
|
||
Convey("should return an error", func() { | ||
So(err, ShouldResemble, errors.New("Error adding API key")) | ||
}) | ||
}) | ||
|
||
Convey("when API key comment was added successfully", func() { | ||
comment := "Test API key" | ||
retval := interfaces.APIKey{UserGUID: userID, Comment: comment} | ||
|
||
ctrl := gomock.NewController(t) | ||
defer ctrl.Finish() | ||
|
||
m := apikeys.NewMockRepository(ctrl) | ||
m. | ||
EXPECT(). | ||
AddAPIKey(gomock.Eq(userID), gomock.Eq(comment)). | ||
Return(&retval, nil) | ||
|
||
req := setupMockReq("POST", "", map[string]string{ | ||
"comment": comment, | ||
}) | ||
|
||
rec, _, ctx, pp, db, _ := setupHTTPTest(req) | ||
defer db.Close() | ||
|
||
pp.APIKeysRepository = m | ||
|
||
ctx.Set("user_id", userID) | ||
|
||
err := pp.addAPIKey(ctx) | ||
|
||
var data map[string]interface{} | ||
if jsonErr := json.Unmarshal(rec.Body.Bytes(), &data); jsonErr != nil { | ||
panic(jsonErr) | ||
} | ||
|
||
Convey("there should be no error", func() { | ||
So(err, ShouldBeNil) | ||
}) | ||
|
||
Convey("should return HTTP code 200", func() { | ||
So(rec.Code, ShouldEqual, 200) | ||
}) | ||
|
||
Convey("API key user_guid should equal context user", func() { | ||
So(data["user_guid"], ShouldEqual, userID) | ||
}) | ||
|
||
Convey("API key comment should equal request comment", func() { | ||
So(data["comment"], ShouldEqual, comment) | ||
}) | ||
|
||
Convey("API key last_used should be nil", func() { | ||
So(data["last_used"], ShouldBeNil) | ||
}) | ||
}) | ||
|
||
}) | ||
} | ||
|
||
func Test_listAPIKeys(t *testing.T) { | ||
t.Parallel() | ||
|
||
Convey("Given a request to list API keys", t, func() { | ||
log.SetLevel(log.WarnLevel) | ||
|
||
userID := "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" | ||
|
||
Convey("when a DB error occurs", func() { | ||
ctrl := gomock.NewController(t) | ||
defer ctrl.Finish() | ||
|
||
m := apikeys.NewMockRepository(ctrl) | ||
m. | ||
EXPECT(). | ||
ListAPIKeys(gomock.Eq(userID)). | ||
Return(nil, errors.New("Something went wrong")) | ||
|
||
req := setupMockReq("GET", "", map[string]string{}) | ||
|
||
_, _, ctx, pp, db, _ := setupHTTPTest(req) | ||
defer db.Close() | ||
pp.APIKeysRepository = m | ||
|
||
ctx.Set("user_id", userID) | ||
|
||
err := pp.listAPIKeys(ctx) | ||
|
||
Convey("should return an error", func() { | ||
So(err, ShouldResemble, errors.New("Error listing API keys")) | ||
}) | ||
}) | ||
|
||
Convey("when DB no errors occur", func() { | ||
r1 := &interfaces.APIKey{ | ||
GUID: "00000000-0000-0000-0000-000000000000", | ||
Secret: "", | ||
UserGUID: userID, | ||
Comment: "First key", | ||
LastUsed: nil, | ||
} | ||
|
||
r2 := &interfaces.APIKey{ | ||
GUID: "11111111-1111-1111-1111-111111111111", | ||
Secret: "", | ||
UserGUID: userID, | ||
Comment: "Second key", | ||
LastUsed: nil, | ||
} | ||
|
||
retval := []interfaces.APIKey{*r1, *r2} | ||
|
||
ctrl := gomock.NewController(t) | ||
defer ctrl.Finish() | ||
|
||
m := apikeys.NewMockRepository(ctrl) | ||
m. | ||
EXPECT(). | ||
ListAPIKeys(gomock.Eq(userID)). | ||
Return(retval, nil) | ||
|
||
req := setupMockReq("GET", "", map[string]string{}) | ||
|
||
rec, _, ctx, pp, db, _ := setupHTTPTest(req) | ||
defer db.Close() | ||
pp.APIKeysRepository = m | ||
|
||
ctx.Set("user_id", userID) | ||
|
||
err := pp.listAPIKeys(ctx) | ||
|
||
Convey("there should be no error", func() { | ||
So(err, ShouldBeNil) | ||
}) | ||
|
||
Convey("return valid JSON", func() { | ||
So(rec.Body.String(), ShouldEqual, jsonMust(retval)+"\n") | ||
}) | ||
}) | ||
}) | ||
} | ||
|
||
func Test_deleteAPIKeys(t *testing.T) { | ||
t.Parallel() | ||
|
||
userID := "aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa" | ||
keyID := "bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb" | ||
|
||
Convey("Given a request to delete an API key", t, func() { | ||
log.SetLevel(log.PanicLevel) | ||
|
||
Convey("when no API key GUID is supplied", func() { | ||
req := setupMockReq("POST", "", map[string]string{ | ||
"guid": "", | ||
}) | ||
|
||
_, _, ctx, pp, db, _ := setupHTTPTest(req) | ||
defer db.Close() | ||
|
||
ctx.Set("user_id", userID) | ||
|
||
err := pp.deleteAPIKey(ctx) | ||
|
||
Convey("should return an error", func() { | ||
So(err, ShouldResemble, echo.NewHTTPError(http.StatusBadRequest, "API key guid can't be empty")) | ||
}) | ||
}) | ||
|
||
Convey("when an error occured during API key deletion", func() { | ||
ctrl := gomock.NewController(t) | ||
defer ctrl.Finish() | ||
|
||
m := apikeys.NewMockRepository(ctrl) | ||
m. | ||
EXPECT(). | ||
DeleteAPIKey(gomock.Eq(userID), gomock.Eq(keyID)). | ||
Return(errors.New("Something went wrong")) | ||
|
||
req := setupMockReq("POST", "", map[string]string{ | ||
"guid": keyID, | ||
}) | ||
|
||
_, _, ctx, pp, db, _ := setupHTTPTest(req) | ||
defer db.Close() | ||
|
||
pp.APIKeysRepository = m | ||
|
||
ctx.Set("user_id", userID) | ||
|
||
err := pp.deleteAPIKey(ctx) | ||
|
||
Convey("should return an error", func() { | ||
So(err, ShouldResemble, errors.New("Error deleting API key")) | ||
}) | ||
}) | ||
|
||
Convey("when an API key is deleted succesfully", func() { | ||
ctrl := gomock.NewController(t) | ||
defer ctrl.Finish() | ||
|
||
m := apikeys.NewMockRepository(ctrl) | ||
m. | ||
EXPECT(). | ||
DeleteAPIKey(gomock.Eq(userID), gomock.Eq(keyID)). | ||
Return(nil) | ||
|
||
req := setupMockReq("POST", "", map[string]string{ | ||
"guid": keyID, | ||
}) | ||
|
||
_, _, ctx, pp, db, _ := setupHTTPTest(req) | ||
defer db.Close() | ||
|
||
pp.APIKeysRepository = m | ||
|
||
ctx.Set("user_id", userID) | ||
|
||
err := pp.deleteAPIKey(ctx) | ||
|
||
Convey("there should be no error", func() { | ||
So(err, ShouldBeNil) | ||
}) | ||
}) | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
package datastore | ||
|
||
import ( | ||
"database/sql" | ||
|
||
"bitbucket.org/liamstask/goose/lib/goose" | ||
) | ||
|
||
func init() { | ||
RegisterMigration(20200814140918, "ApiKeys", func(txn *sql.Tx, conf *goose.DBConf) error { | ||
apiTokenTable := "CREATE TABLE IF NOT EXISTS api_keys (" | ||
apiTokenTable += "guid VARCHAR(36) NOT NULL UNIQUE," | ||
apiTokenTable += "secret VARCHAR(36) NOT NULL UNIQUE," | ||
apiTokenTable += "user_guid VARCHAR(36) NOT NULL," | ||
apiTokenTable += "comment VARCHAR(255) NOT NULL," | ||
apiTokenTable += "last_used TIMESTAMP," | ||
apiTokenTable += "PRIMARY KEY (guid) );" | ||
|
||
_, err := txn.Exec(apiTokenTable) | ||
return err | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.