Skip to content

Commit 2581044

Browse files
authored
feat(api, cli, ui): auth consumer token expiration and last authentication (#5822)
1 parent b746891 commit 2581044

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+664
-227
lines changed

cli/cdsctl/consumer.go

+39-9
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"fmt"
5+
"time"
56

67
"github.com/pkg/errors"
78
"github.com/spf13/cobra"
@@ -86,6 +87,9 @@ var authConsumerNewCmd = cli.Command{
8687
Name: "scopes",
8788
Type: cli.FlagSlice,
8889
Usage: "Define the list of scopes for the consumer",
90+
}, {
91+
Name: "duration",
92+
Usage: "Validity period of the token generated for the consumer (in days)",
8993
},
9094
},
9195
}
@@ -121,7 +125,7 @@ func authConsumerNewRun(v cli.Values) error {
121125
}
122126
}
123127
if !found {
124-
return errors.Errorf("invalid given group name: '%s'", g)
128+
return errors.Errorf("invalid given group name: %q", g)
125129
}
126130
}
127131
if len(groupIDs) == 0 && !v.GetBool("no-interactive") {
@@ -139,7 +143,7 @@ func authConsumerNewRun(v cli.Values) error {
139143
for _, s := range v.GetStringSlice("scopes") {
140144
scope := sdk.AuthConsumerScope(s)
141145
if !scope.IsValid() {
142-
return errors.Errorf("invalid given scope value: '%s'", scope)
146+
return errors.Errorf("invalid given scope value: %q", scope)
143147
}
144148
scopes = append(scopes, scope)
145149
}
@@ -154,11 +158,21 @@ func authConsumerNewRun(v cli.Values) error {
154158
}
155159
}
156160

161+
var duration time.Duration
162+
if v.GetString("duration") != "" {
163+
iDuration, err := v.GetInt64("duration")
164+
if err != nil {
165+
return errors.Errorf("invalid given duration: %q", v.GetString("duration"))
166+
}
167+
duration = time.Duration(iDuration) * (24 * time.Hour)
168+
}
169+
157170
res, err := client.AuthConsumerCreateForUser(username, sdk.AuthConsumer{
158-
Name: name,
159-
Description: description,
160-
GroupIDs: groupIDs,
161-
ScopeDetails: sdk.NewAuthConsumerScopeDetails(scopes...),
171+
Name: name,
172+
Description: description,
173+
GroupIDs: groupIDs,
174+
ScopeDetails: sdk.NewAuthConsumerScopeDetails(scopes...),
175+
ValidityPeriods: sdk.NewAuthConsumerValidityPeriod(time.Now(), duration),
162176
})
163177
if err != nil {
164178
return err
@@ -195,7 +209,7 @@ func authConsumerDeleteRun(v cli.Values) error {
195209
if err := client.AuthConsumerDelete(username, consumerID); err != nil {
196210
return err
197211
}
198-
fmt.Printf("Consumer '%s' successfully deleted.\n", consumerID)
212+
fmt.Printf("Consumer %q successfully deleted.\n", consumerID)
199213

200214
return nil
201215
}
@@ -214,6 +228,15 @@ var authConsumerRegenCmd = cli.Command{
214228
Name: consumerIDArg,
215229
},
216230
},
231+
Flags: []cli.Flag{
232+
{
233+
Name: "duration",
234+
Usage: "Validity period of the token generated for the consumer (in days)",
235+
}, {
236+
Name: "overlap",
237+
Usage: "Overlap duration between actual token and the new one. eg: 24h, 30m",
238+
},
239+
},
217240
}
218241

219242
func authConsumerRegenRun(v cli.Values) error {
@@ -222,12 +245,19 @@ func authConsumerRegenRun(v cli.Values) error {
222245
username = "me"
223246
}
224247

248+
duration, err := v.GetInt64("duration")
249+
if err != nil {
250+
return errors.Errorf("invalid given duration: %q", v.GetString("duration"))
251+
}
252+
253+
overlap := v.GetString("overlap")
254+
225255
consumerID := v.GetString(consumerIDArg)
226-
consumer, err := client.AuthConsumerRegen(username, consumerID)
256+
consumer, err := client.AuthConsumerRegen(username, consumerID, duration, overlap)
227257
if err != nil {
228258
return err
229259
}
230-
fmt.Printf("Consumer '%s' successfully regenerated.\n", consumerID)
260+
fmt.Printf("Consumer %q successfully regenerated.\n", consumerID)
231261
fmt.Printf("Token: %s\n", consumer.Token)
232262

233263
return nil

cli/cdsctl/login.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@ func createOrRegenConsumer(apiURL, username, sessionToken string, v cli.Values)
363363
}
364364
}
365365
if consumerID != "" {
366-
consumer, err := client.AuthConsumerRegen(username, consumerID)
366+
consumer, err := client.AuthConsumerRegen(username, consumerID, 0, "")
367367
if err != nil {
368368
return "", "", cli.WrapError(err, "cdsctl: cannot regenerate consumer")
369369
}

engine/api/admin.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -47,8 +47,7 @@ func (api *API) postMaintenanceHandler() service.Handler {
4747

4848
func (api *API) getAdminServicesHandler() service.Handler {
4949
return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
50-
srvs := []sdk.Service{}
51-
50+
var srvs []sdk.Service
5251
var err error
5352
if r.FormValue("type") != "" {
5453
srvs, err = services.LoadAllByType(ctx, api.mustDB(), r.FormValue("type"), services.LoadOptions.WithStatus)

engine/api/api.go

+9-3
Original file line numberDiff line numberDiff line change
@@ -84,9 +84,11 @@ type Configuration struct {
8484
InsecureSkipVerifyTLS bool `toml:"insecureSkipVerifyTLS" json:"insecureSkipVerifyTLS" default:"false"`
8585
} `toml:"internalServiceMesh" json:"internalServiceMesh"`
8686
Auth struct {
87-
DefaultGroup string `toml:"defaultGroup" default:"" comment:"The default group is the group in which every new user will be granted at signup" json:"defaultGroup"`
88-
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:"-"`
89-
LDAP struct {
87+
TokenDefaultDuration int64 `toml:"tokenDefaultDuration" default:"30" comment:"The default duration of a token (in days)" json:"tokenDefaultDuration"`
88+
TokenOverlapDefaultDuration string `toml:"tokenOverlapDefaultDuration" default:"24h" comment:"The default overlap duration when a token is regen" json:"tokenOverlapDefaultDuration"`
89+
DefaultGroup string `toml:"defaultGroup" default:"" comment:"The default group is the group in which every new user will be granted at signup" json:"defaultGroup"`
90+
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:"-"`
91+
LDAP struct {
9092
Enabled bool `toml:"enabled" default:"false" json:"enabled"`
9193
SignupDisabled bool `toml:"signupDisabled" default:"false" json:"signupDisabled"`
9294
Host string `toml:"host" json:"host"`
@@ -710,6 +712,10 @@ func (a *API) Serve(ctx context.Context) error {
710712
return migrate.RunsSecrets(ctx, a.DBConnectionFactory.GetDBMap(gorpmapping.Mapper))
711713
}})
712714

715+
migrate.Add(ctx, sdk.Migration{Name: "AuthConsumerTokenExpiration", Release: "0.47.0", Blocker: true, Automatic: true, ExecFunc: func(ctx context.Context) error {
716+
return migrate.AuthConsumerTokenExpiration(ctx, a.DBConnectionFactory.GetDBMap(gorpmapping.Mapper), time.Duration(a.Config.Auth.TokenDefaultDuration)*(24*time.Hour))
717+
}})
718+
713719
isFreshInstall, errF := version.IsFreshInstall(a.mustDB())
714720
if errF != nil {
715721
return sdk.WrapError(errF, "Unable to check if it's a fresh installation of CDS")

engine/api/application_deployment_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -297,7 +297,7 @@ func Test_postApplicationDeploymentStrategyConfigHandlerAsProvider(t *testing.T)
297297
localConsumer, err := authentication.LoadConsumerByTypeAndUserID(context.TODO(), api.mustDB(), sdk.ConsumerLocal, u.ID, authentication.LoadConsumerOptions.WithAuthentifiedUser)
298298
require.NoError(t, err)
299299

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

303303
pkey := sdk.RandomString(10)

engine/api/application_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ func Test_postApplicationMetadataHandler_AsProvider(t *testing.T) {
3636
u, _ := assets.InsertAdminUser(t, db)
3737
localConsumer, err := authentication.LoadConsumerByTypeAndUserID(context.TODO(), api.mustDB(), sdk.ConsumerLocal, u.ID, authentication.LoadConsumerOptions.WithAuthentifiedUser)
3838
require.NoError(t, err)
39-
_, jws, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), sdk.RandomString(10), localConsumer, u.GetGroupIDs(),
39+
_, jws, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), sdk.RandomString(10), 0, localConsumer, u.GetGroupIDs(),
4040
sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeProject))
4141

4242
pkey := sdk.RandomString(10)

engine/api/auth.go

+8
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package api
33
import (
44
"context"
55
"net/http"
6+
"time"
67

78
"github.com/gorilla/mux"
89
"github.com/rockbears/log"
@@ -273,6 +274,13 @@ func (api *API) postAuthSigninHandler() service.Handler {
273274
return err
274275
}
275276

277+
// Store the last authentication date on the consumer
278+
now := time.Now()
279+
consumer.LastAuthentication = &now
280+
if err := authentication.UpdateConsumerLastAuthentication(ctx, tx, consumer); err != nil {
281+
return err
282+
}
283+
276284
log.Debug(ctx, "postAuthSigninHandler> new session %s created for %.2f seconds: %+v", session.ID, sessionDuration.Seconds(), session)
277285

278286
// Generate a jwt for current session

engine/api/auth_builtin.go

+16-8
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"encoding/base64"
66
"net/http"
7+
"time"
78

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

2425
// Extract and validate signin request
2526
var req sdk.AuthConsumerSigninRequest
2627
if err := service.UnmarshalBody(r, &req); err != nil {
27-
return err
28+
return sdk.NewError(sdk.ErrForbidden, err)
2829
}
2930
if err := driver.CheckSigninRequest(req); err != nil {
30-
return err
31+
return sdk.NewError(sdk.ErrForbidden, err)
3132
}
3233
// Convert code to external user info
3334
userInfo, err := driver.GetUserInfo(ctx, req)
3435
if err != nil {
35-
return err
36+
return sdk.NewError(sdk.ErrForbidden, err)
3637
}
3738

3839
tx, err := api.mustDB().Begin()
3940
if err != nil {
40-
return sdk.WithStack(err)
41+
return sdk.NewError(sdk.ErrForbidden, err)
4142
}
4243
defer tx.Rollback() // nolint
4344

4445
// Check if a consumer exists for consumer type and external user identifier
4546
consumer, err := authentication.LoadConsumerByID(ctx, tx, userInfo.ExternalID)
4647
if err != nil {
47-
return err
48+
return sdk.NewError(sdk.ErrForbidden, err)
4849
}
4950

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

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

62+
// Store the last authentication date on the consumer
63+
now := time.Now()
64+
consumer.LastAuthentication = &now
65+
if err := authentication.UpdateConsumerLastAuthentication(ctx, tx, consumer); err != nil {
66+
return err
67+
}
68+
6169
// Generate a jwt for current session
6270
jwt, err := authentication.NewSessionJWT(session, "")
6371
if err != nil {

engine/api/auth_builtin_test.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ func Test_postAuthBuiltinSigninHandler(t *testing.T) {
5252
localConsumer, err := authentication.LoadConsumerByTypeAndUserID(context.TODO(), api.mustDB(), sdk.ConsumerLocal, usr.ID, authentication.LoadConsumerOptions.WithAuthentifiedUser)
5353
require.NoError(t, err)
5454

55-
_, jws, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), sdk.RandomString(10), localConsumer, usr.GetGroupIDs(),
55+
_, jws, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), sdk.RandomString(10), 0, localConsumer, usr.GetGroupIDs(),
5656
sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeProject))
5757
require.NoError(t, err)
5858
AuthentififyBuiltinConsumer(t, api, jws)

engine/api/auth_consumer.go

+31-2
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,11 @@ package api
33
import (
44
"context"
55
"net/http"
6+
"time"
67

78
"github.com/gorilla/mux"
89
"github.com/ovh/cds/sdk"
10+
"github.com/pkg/errors"
911
"github.com/rockbears/log"
1012

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

82+
if reqData.ValidityPeriods.Latest() == nil {
83+
reqData.ValidityPeriods = sdk.NewAuthConsumerValidityPeriod(time.Now(), time.Duration(api.Config.Auth.TokenDefaultDuration)*(24*time.Hour))
84+
}
85+
8086
// Create the new built in consumer from request data
81-
newConsumer, token, err := builtin.NewConsumer(ctx, tx, reqData.Name, reqData.Description,
87+
newConsumer, token, err := builtin.NewConsumer(ctx, tx, reqData.Name, reqData.Description, reqData.ValidityPeriods.Latest().Duration,
8288
consumer, reqData.GroupIDs, reqData.ScopeDetails)
8389
if err != nil {
8490
return err
@@ -153,7 +159,30 @@ func (api *API) postConsumerRegenByUserHandler() service.Handler {
153159
return err
154160
}
155161

156-
if err := authentication.ConsumerRegen(ctx, tx, consumer); err != nil {
162+
if req.OverlapDuration == "" {
163+
req.OverlapDuration = api.Config.Auth.TokenOverlapDefaultDuration
164+
}
165+
if req.NewDuration == 0 {
166+
req.NewDuration = api.Config.Auth.TokenDefaultDuration
167+
}
168+
var overlapDuration time.Duration
169+
if req.OverlapDuration != "" {
170+
overlapDuration, err = time.ParseDuration(req.OverlapDuration)
171+
if err != nil {
172+
return sdk.NewError(sdk.ErrWrongRequest, err)
173+
}
174+
}
175+
176+
newDuration := time.Duration(req.NewDuration) * (24 * time.Hour)
177+
178+
if overlapDuration > newDuration {
179+
return sdk.NewError(sdk.ErrWrongRequest, errors.New("invalid duration"))
180+
}
181+
182+
if err := authentication.ConsumerRegen(ctx, tx, consumer,
183+
overlapDuration,
184+
newDuration,
185+
); err != nil {
157186
return err
158187
}
159188

0 commit comments

Comments
 (0)