-
Notifications
You must be signed in to change notification settings - Fork 432
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(api): access token with jwt authentication (#3844)
* 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
Showing
102 changed files
with
3,070 additions
and
618 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,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) | ||
} | ||
} |
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,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()) | ||
|
||
} |
Oops, something went wrong.