Skip to content

Commit

Permalink
feat(api, cli, ui): auth consumer token expiration and last authentic…
Browse files Browse the repository at this point in the history
…ation (#5822)
  • Loading branch information
fsamin authored Jun 16, 2021
1 parent b746891 commit 2581044
Show file tree
Hide file tree
Showing 50 changed files with 664 additions and 227 deletions.
48 changes: 39 additions & 9 deletions cli/cdsctl/consumer.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package main

import (
"fmt"
"time"

"github.com/pkg/errors"
"github.com/spf13/cobra"
Expand Down Expand Up @@ -86,6 +87,9 @@ var authConsumerNewCmd = cli.Command{
Name: "scopes",
Type: cli.FlagSlice,
Usage: "Define the list of scopes for the consumer",
}, {
Name: "duration",
Usage: "Validity period of the token generated for the consumer (in days)",
},
},
}
Expand Down Expand Up @@ -121,7 +125,7 @@ func authConsumerNewRun(v cli.Values) error {
}
}
if !found {
return errors.Errorf("invalid given group name: '%s'", g)
return errors.Errorf("invalid given group name: %q", g)
}
}
if len(groupIDs) == 0 && !v.GetBool("no-interactive") {
Expand All @@ -139,7 +143,7 @@ func authConsumerNewRun(v cli.Values) error {
for _, s := range v.GetStringSlice("scopes") {
scope := sdk.AuthConsumerScope(s)
if !scope.IsValid() {
return errors.Errorf("invalid given scope value: '%s'", scope)
return errors.Errorf("invalid given scope value: %q", scope)
}
scopes = append(scopes, scope)
}
Expand All @@ -154,11 +158,21 @@ func authConsumerNewRun(v cli.Values) error {
}
}

var duration time.Duration
if v.GetString("duration") != "" {
iDuration, err := v.GetInt64("duration")
if err != nil {
return errors.Errorf("invalid given duration: %q", v.GetString("duration"))
}
duration = time.Duration(iDuration) * (24 * time.Hour)
}

res, err := client.AuthConsumerCreateForUser(username, sdk.AuthConsumer{
Name: name,
Description: description,
GroupIDs: groupIDs,
ScopeDetails: sdk.NewAuthConsumerScopeDetails(scopes...),
Name: name,
Description: description,
GroupIDs: groupIDs,
ScopeDetails: sdk.NewAuthConsumerScopeDetails(scopes...),
ValidityPeriods: sdk.NewAuthConsumerValidityPeriod(time.Now(), duration),
})
if err != nil {
return err
Expand Down Expand Up @@ -195,7 +209,7 @@ func authConsumerDeleteRun(v cli.Values) error {
if err := client.AuthConsumerDelete(username, consumerID); err != nil {
return err
}
fmt.Printf("Consumer '%s' successfully deleted.\n", consumerID)
fmt.Printf("Consumer %q successfully deleted.\n", consumerID)

return nil
}
Expand All @@ -214,6 +228,15 @@ var authConsumerRegenCmd = cli.Command{
Name: consumerIDArg,
},
},
Flags: []cli.Flag{
{
Name: "duration",
Usage: "Validity period of the token generated for the consumer (in days)",
}, {
Name: "overlap",
Usage: "Overlap duration between actual token and the new one. eg: 24h, 30m",
},
},
}

func authConsumerRegenRun(v cli.Values) error {
Expand All @@ -222,12 +245,19 @@ func authConsumerRegenRun(v cli.Values) error {
username = "me"
}

duration, err := v.GetInt64("duration")
if err != nil {
return errors.Errorf("invalid given duration: %q", v.GetString("duration"))
}

overlap := v.GetString("overlap")

consumerID := v.GetString(consumerIDArg)
consumer, err := client.AuthConsumerRegen(username, consumerID)
consumer, err := client.AuthConsumerRegen(username, consumerID, duration, overlap)
if err != nil {
return err
}
fmt.Printf("Consumer '%s' successfully regenerated.\n", consumerID)
fmt.Printf("Consumer %q successfully regenerated.\n", consumerID)
fmt.Printf("Token: %s\n", consumer.Token)

return nil
Expand Down
2 changes: 1 addition & 1 deletion cli/cdsctl/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -363,7 +363,7 @@ func createOrRegenConsumer(apiURL, username, sessionToken string, v cli.Values)
}
}
if consumerID != "" {
consumer, err := client.AuthConsumerRegen(username, consumerID)
consumer, err := client.AuthConsumerRegen(username, consumerID, 0, "")
if err != nil {
return "", "", cli.WrapError(err, "cdsctl: cannot regenerate consumer")
}
Expand Down
3 changes: 1 addition & 2 deletions engine/api/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,7 @@ func (api *API) postMaintenanceHandler() service.Handler {

func (api *API) getAdminServicesHandler() service.Handler {
return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
srvs := []sdk.Service{}

var srvs []sdk.Service
var err error
if r.FormValue("type") != "" {
srvs, err = services.LoadAllByType(ctx, api.mustDB(), r.FormValue("type"), services.LoadOptions.WithStatus)
Expand Down
12 changes: 9 additions & 3 deletions engine/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,9 +84,11 @@ type Configuration struct {
InsecureSkipVerifyTLS bool `toml:"insecureSkipVerifyTLS" json:"insecureSkipVerifyTLS" default:"false"`
} `toml:"internalServiceMesh" json:"internalServiceMesh"`
Auth struct {
DefaultGroup string `toml:"defaultGroup" default:"" comment:"The default group is the group in which every new user will be granted at signup" json:"defaultGroup"`
RSAPrivateKey string `toml:"rsaPrivateKey" default:"" comment:"The RSA Private Key used to sign and verify the JWT Tokens issued by the API \nThis is mandatory." json:"-"`
LDAP struct {
TokenDefaultDuration int64 `toml:"tokenDefaultDuration" default:"30" comment:"The default duration of a token (in days)" json:"tokenDefaultDuration"`
TokenOverlapDefaultDuration string `toml:"tokenOverlapDefaultDuration" default:"24h" comment:"The default overlap duration when a token is regen" json:"tokenOverlapDefaultDuration"`
DefaultGroup string `toml:"defaultGroup" default:"" comment:"The default group is the group in which every new user will be granted at signup" json:"defaultGroup"`
RSAPrivateKey string `toml:"rsaPrivateKey" default:"" comment:"The RSA Private Key used to sign and verify the JWT Tokens issued by the API \nThis is mandatory." json:"-"`
LDAP struct {
Enabled bool `toml:"enabled" default:"false" json:"enabled"`
SignupDisabled bool `toml:"signupDisabled" default:"false" json:"signupDisabled"`
Host string `toml:"host" json:"host"`
Expand Down Expand Up @@ -710,6 +712,10 @@ func (a *API) Serve(ctx context.Context) error {
return migrate.RunsSecrets(ctx, a.DBConnectionFactory.GetDBMap(gorpmapping.Mapper))
}})

migrate.Add(ctx, sdk.Migration{Name: "AuthConsumerTokenExpiration", Release: "0.47.0", Blocker: true, Automatic: true, ExecFunc: func(ctx context.Context) error {
return migrate.AuthConsumerTokenExpiration(ctx, a.DBConnectionFactory.GetDBMap(gorpmapping.Mapper), time.Duration(a.Config.Auth.TokenDefaultDuration)*(24*time.Hour))
}})

isFreshInstall, errF := version.IsFreshInstall(a.mustDB())
if errF != nil {
return sdk.WrapError(errF, "Unable to check if it's a fresh installation of CDS")
Expand Down
2 changes: 1 addition & 1 deletion engine/api/application_deployment_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -297,7 +297,7 @@ func Test_postApplicationDeploymentStrategyConfigHandlerAsProvider(t *testing.T)
localConsumer, err := authentication.LoadConsumerByTypeAndUserID(context.TODO(), api.mustDB(), sdk.ConsumerLocal, u.ID, authentication.LoadConsumerOptions.WithAuthentifiedUser)
require.NoError(t, err)

_, jws, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), sdk.RandomString(10), localConsumer, u.GetGroupIDs(),
_, jws, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), sdk.RandomString(10), 0, localConsumer, u.GetGroupIDs(),
sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeProject))

pkey := sdk.RandomString(10)
Expand Down
2 changes: 1 addition & 1 deletion engine/api/application_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ func Test_postApplicationMetadataHandler_AsProvider(t *testing.T) {
u, _ := assets.InsertAdminUser(t, db)
localConsumer, err := authentication.LoadConsumerByTypeAndUserID(context.TODO(), api.mustDB(), sdk.ConsumerLocal, u.ID, authentication.LoadConsumerOptions.WithAuthentifiedUser)
require.NoError(t, err)
_, jws, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), sdk.RandomString(10), localConsumer, u.GetGroupIDs(),
_, jws, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), sdk.RandomString(10), 0, localConsumer, u.GetGroupIDs(),
sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeProject))

pkey := sdk.RandomString(10)
Expand Down
8 changes: 8 additions & 0 deletions engine/api/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package api
import (
"context"
"net/http"
"time"

"github.com/gorilla/mux"
"github.com/rockbears/log"
Expand Down Expand Up @@ -273,6 +274,13 @@ func (api *API) postAuthSigninHandler() service.Handler {
return err
}

// Store the last authentication date on the consumer
now := time.Now()
consumer.LastAuthentication = &now
if err := authentication.UpdateConsumerLastAuthentication(ctx, tx, consumer); err != nil {
return err
}

log.Debug(ctx, "postAuthSigninHandler> new session %s created for %.2f seconds: %+v", session.ID, sessionDuration.Seconds(), session)

// Generate a jwt for current session
Expand Down
24 changes: 16 additions & 8 deletions engine/api/auth_builtin.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"encoding/base64"
"net/http"
"time"

"github.com/ovh/cds/engine/api/authentication"
"github.com/ovh/cds/engine/api/authentication/builtin"
Expand All @@ -18,38 +19,38 @@ func (api *API) postAuthBuiltinSigninHandler() service.Handler {
// Get the consumer builtin driver
driver, ok := api.AuthenticationDrivers[sdk.ConsumerBuiltin]
if !ok {
return sdk.WithStack(sdk.ErrNotFound)
return sdk.WithStack(sdk.ErrForbidden)
}

// Extract and validate signin request
var req sdk.AuthConsumerSigninRequest
if err := service.UnmarshalBody(r, &req); err != nil {
return err
return sdk.NewError(sdk.ErrForbidden, err)
}
if err := driver.CheckSigninRequest(req); err != nil {
return err
return sdk.NewError(sdk.ErrForbidden, err)
}
// Convert code to external user info
userInfo, err := driver.GetUserInfo(ctx, req)
if err != nil {
return err
return sdk.NewError(sdk.ErrForbidden, err)
}

tx, err := api.mustDB().Begin()
if err != nil {
return sdk.WithStack(err)
return sdk.NewError(sdk.ErrForbidden, err)
}
defer tx.Rollback() // nolint

// Check if a consumer exists for consumer type and external user identifier
consumer, err := authentication.LoadConsumerByID(ctx, tx, userInfo.ExternalID)
if err != nil {
return err
return sdk.NewError(sdk.ErrForbidden, err)
}

// Check the Token validity againts the IAT attribute
if _, err := builtin.CheckSigninConsumerTokenIssuedAt(req["token"], consumer.IssuedAt); err != nil {
return err
if _, err := builtin.CheckSigninConsumerTokenIssuedAt(ctx, req["token"], consumer); err != nil {
return sdk.NewError(sdk.ErrForbidden, err)
}

// Generate a new session for consumer
Expand All @@ -58,6 +59,13 @@ func (api *API) postAuthBuiltinSigninHandler() service.Handler {
return err
}

// Store the last authentication date on the consumer
now := time.Now()
consumer.LastAuthentication = &now
if err := authentication.UpdateConsumerLastAuthentication(ctx, tx, consumer); err != nil {
return err
}

// Generate a jwt for current session
jwt, err := authentication.NewSessionJWT(session, "")
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion engine/api/auth_builtin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func Test_postAuthBuiltinSigninHandler(t *testing.T) {
localConsumer, err := authentication.LoadConsumerByTypeAndUserID(context.TODO(), api.mustDB(), sdk.ConsumerLocal, usr.ID, authentication.LoadConsumerOptions.WithAuthentifiedUser)
require.NoError(t, err)

_, jws, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), sdk.RandomString(10), localConsumer, usr.GetGroupIDs(),
_, jws, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), sdk.RandomString(10), 0, localConsumer, usr.GetGroupIDs(),
sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeProject))
require.NoError(t, err)
AuthentififyBuiltinConsumer(t, api, jws)
Expand Down
33 changes: 31 additions & 2 deletions engine/api/auth_consumer.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package api
import (
"context"
"net/http"
"time"

"github.com/gorilla/mux"
"github.com/ovh/cds/sdk"
"github.com/pkg/errors"
"github.com/rockbears/log"

"github.com/ovh/cds/engine/api/authentication"
Expand Down Expand Up @@ -77,8 +79,12 @@ func (api *API) postConsumerByUserHandler() service.Handler {
return err
}

if reqData.ValidityPeriods.Latest() == nil {
reqData.ValidityPeriods = sdk.NewAuthConsumerValidityPeriod(time.Now(), time.Duration(api.Config.Auth.TokenDefaultDuration)*(24*time.Hour))
}

// Create the new built in consumer from request data
newConsumer, token, err := builtin.NewConsumer(ctx, tx, reqData.Name, reqData.Description,
newConsumer, token, err := builtin.NewConsumer(ctx, tx, reqData.Name, reqData.Description, reqData.ValidityPeriods.Latest().Duration,
consumer, reqData.GroupIDs, reqData.ScopeDetails)
if err != nil {
return err
Expand Down Expand Up @@ -153,7 +159,30 @@ func (api *API) postConsumerRegenByUserHandler() service.Handler {
return err
}

if err := authentication.ConsumerRegen(ctx, tx, consumer); err != nil {
if req.OverlapDuration == "" {
req.OverlapDuration = api.Config.Auth.TokenOverlapDefaultDuration
}
if req.NewDuration == 0 {
req.NewDuration = api.Config.Auth.TokenDefaultDuration
}
var overlapDuration time.Duration
if req.OverlapDuration != "" {
overlapDuration, err = time.ParseDuration(req.OverlapDuration)
if err != nil {
return sdk.NewError(sdk.ErrWrongRequest, err)
}
}

newDuration := time.Duration(req.NewDuration) * (24 * time.Hour)

if overlapDuration > newDuration {
return sdk.NewError(sdk.ErrWrongRequest, errors.New("invalid duration"))
}

if err := authentication.ConsumerRegen(ctx, tx, consumer,
overlapDuration,
newDuration,
); err != nil {
return err
}

Expand Down
Loading

0 comments on commit 2581044

Please sign in to comment.