Skip to content

Commit

Permalink
feat(api): access token with jwt authentication (#3844)
Browse files Browse the repository at this point in the history
* wip

* wip

* wip

* wip

* wip

* wip

* wip

* wip

* fix cr

* fix cr

* fix order import

* fix comment

* fix(sql): primary key on access_token_group

* Update engine/api/accesstoken/accesstoken.go

Co-Authored-By: fsamin <[email protected]>

* fix cr

* fix(sql): rename sql file

* fix merge
  • Loading branch information
fsamin authored and richardlt committed Jan 16, 2019
1 parent cb43a65 commit ef143f5
Show file tree
Hide file tree
Showing 102 changed files with 3,070 additions and 618 deletions.
164 changes: 164 additions & 0 deletions engine/api/access_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
package api

import (
"context"
"net/http"
"time"

"github.com/gorilla/mux"

"github.com/ovh/cds/engine/api/accesstoken"
"github.com/ovh/cds/engine/api/group"
"github.com/ovh/cds/engine/service"
"github.com/ovh/cds/sdk"
"github.com/ovh/cds/sdk/log"
)

// Manage access token handlers

// postNewAccessTokenHandler create a new specific accesstoken with a specific scope (list of groups)
// the JWT token is send through a header X-CDS-JWT
func (api *API) postNewAccessTokenHandler() service.Handler {
return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
// the groupIDs are the scope of the requested token
var accessTokenRequest sdk.AccessTokenRequest
if err := service.UnmarshalBody(r, &accessTokenRequest); err != nil {
return sdk.WithStack(err)
}

// if the scope is empty, raise an error
if len(accessTokenRequest.GroupsIDs) == 0 {
return sdk.WithStack(sdk.ErrWrongRequest)
}

grantedUser := getGrantedUser(ctx)

tx, err := api.mustDB().Begin()
if err != nil {
return sdk.WithStack(err)
}

defer tx.Rollback() // nolint

allGroups, err := group.LoadGroupByAdmin(tx, grantedUser.OnBehalfOf.ID)
if err != nil {
return sdk.WithStack(err)
}

// check that provided group is among the allGroups slice
// a user can create a token associated to a group only if he is admin of this group
var scopeGroup = make([]sdk.Group, 0, len(accessTokenRequest.GroupsIDs))
for _, groupID := range accessTokenRequest.GroupsIDs {
var found bool
for _, g := range allGroups {
if g.ID == groupID {
found = true
scopeGroup = append(scopeGroup, g)
break
}
}
if !found {
return sdk.WrapError(sdk.ErrWrongRequest, "group %d not found", groupID)
}
}

var expiration *time.Time
if accessTokenRequest.ExpirationDelaySecond > 0 {
t := time.Now().Add(time.Duration(accessTokenRequest.ExpirationDelaySecond) * time.Second)
expiration = &t
}

// Create the token
token, jwttoken, err := accesstoken.New(grantedUser.OnBehalfOf, scopeGroup, accessTokenRequest.Origin, accessTokenRequest.Description, expiration)
if err != nil {
return sdk.WithStack(err)
}

// Insert the token
if err := accesstoken.Insert(tx, &token); err != nil {
return sdk.WithStack(err)
}

// Commit the token
if err := tx.Commit(); err != nil {
return sdk.WithStack(err)
}

// Set the JWT token as a header
log.Debug("token.postNewAccessTokenHandler> X-CDS-JWT:%s", jwttoken[:12])
w.Header().Add("X-CDS-JWT", jwttoken)

return service.WriteJSON(w, token, http.StatusCreated)
}
}

// putRegenAccessTokenHandler create a new specific accesstoken with a specific scope (list of groups)
// the JWT token is send through a header X-CDS-JWT
func (api *API) putRegenAccessTokenHandler() service.Handler {
return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
id := vars["id"]

// the groupIDs are the scope of the requested token
var accessTokenRequest sdk.AccessTokenRequest
if err := service.UnmarshalBody(r, &accessTokenRequest); err != nil {
return sdk.WithStack(err)
}

// if the scope is empty, raise an error
if len(accessTokenRequest.GroupsIDs) == 0 {
return sdk.WithStack(sdk.ErrWrongRequest)
}

tx, err := api.mustDB().Begin()
if err != nil {
return sdk.WithStack(err)
}

t, err := accesstoken.FindByID(tx, id)
if err != nil {
return sdk.WithStack(err)
}

// Create the token
jwttoken, err := accesstoken.Regen(&t)
if err != nil {
return sdk.WithStack(err)
}

// Set the JWT token as a header
w.Header().Add("X-CDS-JWT", jwttoken)

return service.WriteJSON(w, t, http.StatusOK)
}
}

func (api *API) getAccessTokenByUserHandler() service.Handler {
return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
id, err := requestVarInt(r, "id")
if err != nil {
return sdk.WithStack(err)
}

tokens, err := accesstoken.FindAllByUser(api.mustDB(), id)
if err != nil {
return sdk.WithStack(err)
}
return service.WriteJSON(w, tokens, http.StatusOK)
}
}

func (api *API) getAccessTokenByGroupHandler() service.Handler {
return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
id, err := requestVarInt(r, "id")
if err != nil {
return sdk.WithStack(err)
}

tokens, err := accesstoken.FindAllByGroup(api.mustDB(), id)
if err != nil {
return sdk.WithStack(err)
}
return service.WriteJSON(w, tokens, http.StatusOK)
}
}
105 changes: 105 additions & 0 deletions engine/api/access_token_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package api

import (
"encoding/json"
"net/http/httptest"
"strconv"
"testing"

"github.com/stretchr/testify/assert"

"github.com/ovh/cds/engine/api/group"
"github.com/ovh/cds/engine/api/test"
"github.com/ovh/cds/engine/api/test/assets"
"github.com/ovh/cds/sdk"
)

func TestAPI_TokenHandlers(t *testing.T) {
api, db, router, end := newTestAPI(t)
defer end()

grp := sdk.Group{Name: sdk.RandomString(10)}
user, password := assets.InsertLambdaUser(db, &grp)
test.NoError(t, group.SetUserGroupAdmin(db, grp.ID, user.ID))

// Test a call with a JWT Token
jwt, err := assets.NewJWTToken(t, db, *user, grp)
test.NoError(t, err)
uri := router.GetRoute("POST", api.postNewAccessTokenHandler, nil)
req := assets.NewJWTAuthentifiedRequest(t, jwt, "POST", uri,
sdk.AccessTokenRequest{
Origin: "test",
Description: "test",
ExpirationDelaySecond: 3600,
GroupsIDs: []int64{grp.ID},
},
)
w := httptest.NewRecorder()
router.Mux.ServeHTTP(w, req)
assert.Equal(t, 201, w.Code)

// Test a call with a JWT Token and an XSFR Token
jwtxsrf, xsrf, err := assets.NewJWTTokenWithXSRF(t, db, api.Cache, *user, grp)
test.NoError(t, err)
uri = router.GetRoute("POST", api.postNewAccessTokenHandler, nil)
req = assets.NewXSRFJWTAuthentifiedRequest(t, jwtxsrf, xsrf, "POST", uri,
sdk.AccessTokenRequest{
Origin: "test",
Description: "testxsrf",
ExpirationDelaySecond: 3600,
GroupsIDs: []int64{grp.ID},
},
)
w = httptest.NewRecorder()
router.Mux.ServeHTTP(w, req)
assert.Equal(t, 201, w.Code)

jwtToken := w.Header().Get("X-CDS-JWT")
t.Logf("jwt token is %v...", jwtToken[:12])

var accessToken sdk.AccessToken
test.NoError(t, json.Unmarshal(w.Body.Bytes(), &accessToken))

vars := map[string]string{
"id": accessToken.ID,
}
uri = router.GetRoute("PUT", api.putRegenAccessTokenHandler, vars)
req = assets.NewAuthentifiedRequest(t, user, password, "PUT", uri,
sdk.AccessTokenRequest{
Origin: "test",
Description: "test",
ExpirationDelaySecond: 3600,
GroupsIDs: []int64{grp.ID},
},
)
w = httptest.NewRecorder()
router.Mux.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)

jwtToken = w.Header().Get("X-CDS-JWT")
t.Logf("jwt token is %v...", jwtToken[:12])
t.Logf("access token is : %s", w.Body.String())

// Test_getAccessTokenByGroupHandler
vars = map[string]string{
"id": strconv.FormatInt(grp.ID, 10),
}
uri = router.GetRoute("GET", api.getAccessTokenByGroupHandler, vars)
req = assets.NewAuthentifiedRequest(t, user, password, "GET", uri, nil)
w = httptest.NewRecorder()
router.Mux.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
t.Logf("getAccessTokenByGroupHandler result is : %s", w.Body.String())

// Test_getAccessTokenByUserHandler
vars = map[string]string{
"id": strconv.FormatInt(user.ID, 10),
}
uri = router.GetRoute("GET", api.getAccessTokenByUserHandler, vars)
req = assets.NewAuthentifiedRequest(t, user, password, "GET", uri, nil)
w = httptest.NewRecorder()
router.Mux.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
t.Logf("getAccessTokenByUserHandler result is : %s", w.Body.String())

}
Loading

0 comments on commit ef143f5

Please sign in to comment.