diff --git a/cli/cdsctl/consumer.go b/cli/cdsctl/consumer.go index 6c25231c8b..54781c38c1 100644 --- a/cli/cdsctl/consumer.go +++ b/cli/cdsctl/consumer.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "time" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -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)", }, }, } @@ -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") { @@ -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) } @@ -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 @@ -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 } @@ -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 { @@ -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 diff --git a/cli/cdsctl/login.go b/cli/cdsctl/login.go index 9429444cd6..53590292bd 100644 --- a/cli/cdsctl/login.go +++ b/cli/cdsctl/login.go @@ -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") } diff --git a/engine/api/admin.go b/engine/api/admin.go index 012edc240c..b4819d2683 100644 --- a/engine/api/admin.go +++ b/engine/api/admin.go @@ -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) diff --git a/engine/api/api.go b/engine/api/api.go index 3c2d2e9592..5fc5b822f7 100644 --- a/engine/api/api.go +++ b/engine/api/api.go @@ -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"` @@ -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") diff --git a/engine/api/application_deployment_test.go b/engine/api/application_deployment_test.go index 67e8e84232..acc467ea18 100644 --- a/engine/api/application_deployment_test.go +++ b/engine/api/application_deployment_test.go @@ -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) diff --git a/engine/api/application_test.go b/engine/api/application_test.go index 897c6e286c..9bee64e359 100644 --- a/engine/api/application_test.go +++ b/engine/api/application_test.go @@ -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) diff --git a/engine/api/auth.go b/engine/api/auth.go index 64e2e79aba..c82380b34c 100644 --- a/engine/api/auth.go +++ b/engine/api/auth.go @@ -3,6 +3,7 @@ package api import ( "context" "net/http" + "time" "github.com/gorilla/mux" "github.com/rockbears/log" @@ -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 diff --git a/engine/api/auth_builtin.go b/engine/api/auth_builtin.go index b8239e90f5..1c3935e7cb 100644 --- a/engine/api/auth_builtin.go +++ b/engine/api/auth_builtin.go @@ -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" @@ -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 @@ -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 { diff --git a/engine/api/auth_builtin_test.go b/engine/api/auth_builtin_test.go index 7ee1efdc87..fbaa50699a 100644 --- a/engine/api/auth_builtin_test.go +++ b/engine/api/auth_builtin_test.go @@ -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) diff --git a/engine/api/auth_consumer.go b/engine/api/auth_consumer.go index 184e741058..0935713798 100644 --- a/engine/api/auth_consumer.go +++ b/engine/api/auth_consumer.go @@ -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" @@ -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 @@ -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 } diff --git a/engine/api/auth_consumer_test.go b/engine/api/auth_consumer_test.go index 37d6d03074..71160d3677 100644 --- a/engine/api/auth_consumer_test.go +++ b/engine/api/auth_consumer_test.go @@ -25,7 +25,7 @@ func Test_getConsumersByUserHandler(t *testing.T) { authentication.LoadConsumerOptions.WithAuthentifiedUser) require.NoError(t, err) - consumer, _, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), "", localConsumer, nil, + consumer, _, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), "", 0, localConsumer, nil, sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeUser)) require.NoError(t, err) @@ -69,10 +69,10 @@ func Test_postConsumerByUserHandler(t *testing.T) { _, jwtRawAdmin := assets.InsertAdminUser(t, db) data := sdk.AuthConsumer{ - Name: sdk.RandomString(10), - GroupIDs: []int64{g.ID}, - ScopeDetails: sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeAccessToken), - IssuedAt: time.Now(), + Name: sdk.RandomString(10), + GroupIDs: []int64{g.ID}, + ScopeDetails: sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeAccessToken), + ValidityPeriods: sdk.NewAuthConsumerValidityPeriod(time.Now(), 0), } uri := api.Router.GetRoute(http.MethodPost, api.postConsumerByUserHandler, map[string]string{ @@ -112,7 +112,7 @@ func Test_deleteConsumerByUserHandler(t *testing.T) { localConsumer, err := authentication.LoadConsumerByTypeAndUserID(context.TODO(), db, sdk.ConsumerLocal, u.ID, authentication.LoadConsumerOptions.WithAuthentifiedUser) require.NoError(t, err) - newConsumer, _, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), "", localConsumer, nil, + newConsumer, _, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), "", 0, localConsumer, nil, sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeAccessToken)) require.NoError(t, err) cs, err := authentication.LoadConsumersByUserID(context.TODO(), db, u.ID) @@ -152,7 +152,7 @@ func Test_postConsumerRegenByUserHandler(t *testing.T) { api.Router.Mux.ServeHTTP(rec, req) require.Equal(t, http.StatusForbidden, rec.Code) - builtinConsumer, signinToken1, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), "", localConsumer, nil, + builtinConsumer, signinToken1, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), "", 0, localConsumer, nil, sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeUser, sdk.AuthConsumerScopeAccessToken)) require.NoError(t, err) session, err := authentication.NewSession(context.TODO(), db, builtinConsumer, 5*time.Minute) @@ -221,16 +221,72 @@ func Test_postConsumerRegenByUserHandler(t *testing.T) { }) rec = httptest.NewRecorder() api.Router.Mux.ServeHTTP(rec, req) - require.Equal(t, http.StatusBadRequest, rec.Code) + require.Equal(t, http.StatusForbidden, rec.Code) // the new signing token from the builtin consumer should be fine + signinToken2 := response.Token uri = api.Router.GetRoute(http.MethodPost, api.postAuthBuiltinSigninHandler, nil) req = assets.NewRequest(t, "POST", uri, sdk.AuthConsumerSigninRequest{ - "token": response.Token, + "token": signinToken2, }) rec = httptest.NewRecorder() api.Router.Mux.ServeHTTP(rec, req) require.Equal(t, http.StatusOK, rec.Code) + + t.Log("next use case...") + time.Sleep(2 * time.Second) + + // Regen the latest token with an overlap duration + uri = api.Router.GetRoute(http.MethodPost, api.postConsumerRegenByUserHandler, map[string]string{ + "permUsername": u.Username, + "permConsumerID": builtinConsumer.ID, + }) + req = assets.NewJWTAuthentifiedRequest(t, jwt3, http.MethodPost, uri, sdk.AuthConsumerRegenRequest{ + RevokeSessions: true, + OverlapDuration: "4s", // short 4s overlap + NewDuration: 1, + }) + rec = httptest.NewRecorder() + api.Router.Mux.ServeHTTP(rec, req) + require.Equal(t, http.StatusOK, rec.Code) + + require.NoError(t, json.Unmarshal(rec.Body.Bytes(), &response)) + t.Logf("here is the new consumer: %+v", response) + + signinToken3 := response.Token + + // Wait before using it + time.Sleep(2 * time.Second) + + // the new signing token from the builtin consumer should be fine + uri = api.Router.GetRoute(http.MethodPost, api.postAuthBuiltinSigninHandler, nil) + req = assets.NewRequest(t, "POST", uri, sdk.AuthConsumerSigninRequest{ + "token": signinToken3, + }) + rec = httptest.NewRecorder() + api.Router.Mux.ServeHTTP(rec, req) + require.Equal(t, http.StatusOK, rec.Code) + + // The old token should be ok too, because of the overlap duration + uri = api.Router.GetRoute(http.MethodPost, api.postAuthBuiltinSigninHandler, nil) + req = assets.NewRequest(t, "POST", uri, sdk.AuthConsumerSigninRequest{ + "token": signinToken2, + }) + rec = httptest.NewRecorder() + api.Router.Mux.ServeHTTP(rec, req) + require.Equal(t, http.StatusOK, rec.Code) + + // Now wait for the overlab duration to be over.... + time.Sleep(2 * time.Second) + + // Now, the old token should be rejected + uri = api.Router.GetRoute(http.MethodPost, api.postAuthBuiltinSigninHandler, nil) + req = assets.NewRequest(t, "POST", uri, sdk.AuthConsumerSigninRequest{ + "token": signinToken2, + }) + rec = httptest.NewRecorder() + api.Router.Mux.ServeHTTP(rec, req) + require.Equal(t, http.StatusForbidden, rec.Code) } func Test_getSessionsByUserHandler(t *testing.T) { @@ -241,7 +297,7 @@ func Test_getSessionsByUserHandler(t *testing.T) { authentication.LoadConsumerOptions.WithAuthentifiedUser) require.NoError(t, err) - consumer, _, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), "", localConsumer, nil, + consumer, _, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), "", 0, localConsumer, nil, sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeUser)) require.NoError(t, err) s2, err := authentication.NewSession(context.TODO(), db, consumer, time.Second) @@ -274,7 +330,7 @@ func Test_deleteSessionByUserHandler(t *testing.T) { authentication.LoadConsumerOptions.WithAuthentifiedUser) require.NoError(t, err) - consumer, _, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), "", localConsumer, nil, + consumer, _, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), "", 0, localConsumer, nil, sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeUser)) require.NoError(t, err) s2, err := authentication.NewSession(context.TODO(), db, consumer, time.Second) diff --git a/engine/api/auth_local.go b/engine/api/auth_local.go index 7afa557d84..59c0c433fe 100644 --- a/engine/api/auth_local.go +++ b/engine/api/auth_local.go @@ -150,7 +150,7 @@ func initBuiltinConsumersFromStartupConfig(ctx context.Context, tx gorpmapper.Sq Data: map[string]string{}, GroupIDs: []int64{group.SharedInfraGroup.ID}, ScopeDetails: scopes, - IssuedAt: time.Unix(startupConfig.IAT, 0), + ValidityPeriods: sdk.NewAuthConsumerValidityPeriod(time.Unix(startupConfig.IAT, 0), 2*365*24*time.Hour), // Default validity period is two years } if err := authentication.InsertConsumer(ctx, tx, &c); err != nil { @@ -209,6 +209,13 @@ func (api *API) postAuthLocalSigninHandler() 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 { @@ -329,6 +336,13 @@ func (api *API) postAuthLocalVerifyHandler() service.Handler { } } + // 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 new session for consumer session, err := authentication.NewSession(ctx, tx, consumer, driver.GetSessionDuration()) if err != nil { @@ -492,6 +506,10 @@ func (api *API) postAuthLocalResetHandler() service.Handler { return err } + // Store the last authentication date on the consumer + now := time.Now() + consumer.LastAuthentication = &now + consumer.Data["hash"] = string(hash) if err := authentication.UpdateConsumer(ctx, tx, consumer); err != nil { return err diff --git a/engine/api/authentication/authentication.go b/engine/api/authentication/authentication.go index ca2340154f..847f94a445 100644 --- a/engine/api/authentication/authentication.go +++ b/engine/api/authentication/authentication.go @@ -46,8 +46,8 @@ func VerifyJWT(token *jwt.Token) (interface{}, error) { return getSigner().VerifyJWT(token) } -func SignJWS(content interface{}, duration time.Duration) (string, error) { - return getSigner().SignJWS(content, duration) +func SignJWS(content interface{}, now time.Time, duration time.Duration) (string, error) { + return getSigner().SignJWS(content, now, duration) } func VerifyJWS(signature string, content interface{}) error { diff --git a/engine/api/authentication/authentication_test.go b/engine/api/authentication/authentication_test.go index ce0c754f71..244be7e12d 100644 --- a/engine/api/authentication/authentication_test.go +++ b/engine/api/authentication/authentication_test.go @@ -26,7 +26,7 @@ func TestSignJWS(t *testing.T) { Nonce: time.Now().Unix(), } - token, err := authentication.SignJWS(p, time.Hour) + token, err := authentication.SignJWS(p, time.Now(), time.Hour) require.NoError(t, err) var res myPayload diff --git a/engine/api/authentication/builtin/builtin.go b/engine/api/authentication/builtin/builtin.go index f16aba2e2c..1caf1a441f 100644 --- a/engine/api/authentication/builtin/builtin.go +++ b/engine/api/authentication/builtin/builtin.go @@ -65,7 +65,7 @@ func (d AuthDriver) CheckSigninRequest(req sdk.AuthConsumerSigninRequest) error // NewConsumer returns a new builtin consumer for given data. // The parent consumer should be given with all data loaded including the authentified user. -func NewConsumer(ctx context.Context, db gorpmapper.SqlExecutorWithTx, name, description string, parentConsumer *sdk.AuthConsumer, +func NewConsumer(ctx context.Context, db gorpmapper.SqlExecutorWithTx, name, description string, duration time.Duration, parentConsumer *sdk.AuthConsumer, groupIDs []int64, scopes sdk.AuthConsumerScopeDetails) (*sdk.AuthConsumer, string, error) { if name == "" { return nil, "", sdk.NewErrorFrom(sdk.ErrWrongRequest, "name should be given to create a built in consumer") @@ -100,7 +100,7 @@ func NewConsumer(ctx context.Context, db gorpmapper.SqlExecutorWithTx, name, des Data: map[string]string{}, GroupIDs: groupIDs, ScopeDetails: scopes, - IssuedAt: time.Now(), + ValidityPeriods: sdk.NewAuthConsumerValidityPeriod(time.Now(), duration), } if err := authentication.InsertConsumer(ctx, db, &c); err != nil { diff --git a/engine/api/authentication/builtin/signin.go b/engine/api/authentication/builtin/signin.go index f239859c92..9b7af2bdbb 100644 --- a/engine/api/authentication/builtin/signin.go +++ b/engine/api/authentication/builtin/signin.go @@ -1,10 +1,12 @@ package builtin import ( + "context" "time" "github.com/ovh/cds/engine/api/authentication" "github.com/ovh/cds/sdk" + "github.com/rockbears/log" ) type signinBuiltinConsumerToken struct { @@ -14,11 +16,12 @@ type signinBuiltinConsumerToken struct { // NewSigninConsumerToken returns a token to signin with built in consumer. func NewSigninConsumerToken(c *sdk.AuthConsumer) (string, error) { + latestValidityPeriod := c.ValidityPeriods.Latest() payload := signinBuiltinConsumerToken{ ConsumerID: c.ID, - IAT: c.IssuedAt.Unix(), + IAT: latestValidityPeriod.IssuedAt.Unix(), } - return authentication.SignJWS(payload, 0) // 0 means no expiration time + return authentication.SignJWS(payload, latestValidityPeriod.IssuedAt, latestValidityPeriod.Duration) } func CheckSigninConsumerToken(signature string) (string, int64, error) { @@ -37,13 +40,31 @@ func parseSigninConsumerToken(signature string) (signinBuiltinConsumerToken, err return payload, nil } -func CheckSigninConsumerTokenIssuedAt(signature string, iat time.Time) (string, error) { +func CheckSigninConsumerTokenIssuedAt(ctx context.Context, signature string, c *sdk.AuthConsumer) (string, error) { payload, err := parseSigninConsumerToken(signature) if err != nil { return "", err } - iatUnix := iat.Unix() - if payload.IAT != iatUnix { + for _, period := range c.ValidityPeriods { + s, err := checkSigninConsumerTokenIssuedAt(ctx, payload, period) + if err == nil { + return s, nil + } else { + log.Debug(ctx, "payload IAT %q is not valid in %+v: %v", payload.IAT, period, err) + } + } + return "", sdk.NewErrorFrom(sdk.ErrWrongRequest, "invalid signin token") +} + +func checkSigninConsumerTokenIssuedAt(ctx context.Context, payload signinBuiltinConsumerToken, v sdk.AuthConsumerValidityPeriod) (string, error) { + var eqIAT = payload.IAT == v.IssuedAt.Unix() + var hasRevoke = v.Duration > 0 + var afterRevoke = time.Now().After(v.IssuedAt.Add(v.Duration)) + + if !eqIAT { + return "", sdk.NewErrorFrom(sdk.ErrWrongRequest, "invalid signin token") + } + if hasRevoke && afterRevoke { return "", sdk.NewErrorFrom(sdk.ErrWrongRequest, "invalid signin token") } return payload.ConsumerID, nil diff --git a/engine/api/authentication/consumer.go b/engine/api/authentication/consumer.go index 41e1cf2275..5a5546f1bc 100644 --- a/engine/api/authentication/consumer.go +++ b/engine/api/authentication/consumer.go @@ -24,7 +24,7 @@ func NewConsumerWorker(ctx context.Context, db gorpmapper.SqlExecutorWithTx, nam sdk.AuthConsumerScopeRunExecution, sdk.AuthConsumerScopeService, ), - IssuedAt: time.Now(), + ValidityPeriods: sdk.NewAuthConsumerValidityPeriod(time.Now(), 24*time.Hour), } if err := InsertConsumer(ctx, db, &c); err != nil { @@ -46,7 +46,7 @@ func NewConsumerExternal(ctx context.Context, db gorpmapper.SqlExecutorWithTx, u "username": userInfo.Username, "email": userInfo.Email, }, - IssuedAt: time.Now(), + ValidityPeriods: sdk.NewAuthConsumerValidityPeriod(time.Now(), 0), } if err := InsertConsumer(ctx, db, &c); err != nil { @@ -57,7 +57,7 @@ func NewConsumerExternal(ctx context.Context, db gorpmapper.SqlExecutorWithTx, u } // ConsumerRegen updates a consumer issue date to invalidate old signin token. -func ConsumerRegen(ctx context.Context, db gorpmapper.SqlExecutorWithTx, consumer *sdk.AuthConsumer) error { +func ConsumerRegen(ctx context.Context, db gorpmapper.SqlExecutorWithTx, consumer *sdk.AuthConsumer, overlapDuration, newDuration time.Duration) error { if consumer.Type != sdk.ConsumerBuiltin { return sdk.NewErrorFrom(sdk.ErrForbidden, "can't regen a no builtin consumer") } @@ -69,8 +69,15 @@ func ConsumerRegen(ctx context.Context, db gorpmapper.SqlExecutorWithTx, consume consumer.InvalidGroupIDs = nil consumer.Warnings = nil - // Update the IAT attribute in database - consumer.IssuedAt = time.Now() + // Regen the token + latestPeriod := consumer.ValidityPeriods.Latest() + latestPeriod.Duration = time.Now().Add(overlapDuration).Sub(latestPeriod.IssuedAt) + consumer.ValidityPeriods = append(consumer.ValidityPeriods, + sdk.AuthConsumerValidityPeriod{ + IssuedAt: time.Now(), + Duration: newDuration, + }, + ) if err := UpdateConsumer(ctx, db, consumer); err != nil { return err } diff --git a/engine/api/authentication/consumer_test.go b/engine/api/authentication/consumer_test.go index 3cdcf922be..f6301ac013 100644 --- a/engine/api/authentication/consumer_test.go +++ b/engine/api/authentication/consumer_test.go @@ -37,7 +37,7 @@ func TestConsumerInvalidateGroupForUser_InvalidateOneConsumerGroup(t *testing.T) ScopeDetails: sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeAdmin), GroupIDs: []int64{g1.ID, g2.ID}, AuthentifiedUserID: u.ID, - IssuedAt: time.Now(), + ValidityPeriods: sdk.NewAuthConsumerValidityPeriod(time.Now(), 0), } require.NoError(t, authentication.InsertConsumer(context.TODO(), db, &c)) @@ -77,7 +77,7 @@ func TestConsumerInvalidateGroupForUser_InvalidateOneConsumerGroupForAdmin(t *te ScopeDetails: sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeAdmin), GroupIDs: []int64{g1.ID, g2.ID}, AuthentifiedUserID: u.ID, - IssuedAt: time.Now(), + ValidityPeriods: sdk.NewAuthConsumerValidityPeriod(time.Now(), 0), } require.NoError(t, authentication.InsertConsumer(context.TODO(), db, &c)) @@ -112,7 +112,7 @@ func TestConsumerInvalidateGroupForUser_InvalidateLastConsumerGroup(t *testing.T ScopeDetails: sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeAdmin), GroupIDs: []int64{g1.ID}, AuthentifiedUserID: u.ID, - IssuedAt: time.Now(), + ValidityPeriods: sdk.NewAuthConsumerValidityPeriod(time.Now(), 0), } require.NoError(t, authentication.InsertConsumer(context.TODO(), db, &c)) @@ -152,7 +152,7 @@ func TestConsumerRemoveGroup_RemoveOneConsumerGroup(t *testing.T) { ScopeDetails: sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeAdmin), GroupIDs: []int64{g1.ID, g2.ID}, AuthentifiedUserID: u.ID, - IssuedAt: time.Now(), + ValidityPeriods: sdk.NewAuthConsumerValidityPeriod(time.Now(), 0), } require.NoError(t, authentication.InsertConsumer(context.TODO(), db, &c)) @@ -191,7 +191,7 @@ func TestConsumerRemoveGroup_RemoveOneInvalidConsumerGroup(t *testing.T) { GroupIDs: []int64{g2.ID}, InvalidGroupIDs: []int64{g1.ID}, AuthentifiedUserID: u.ID, - IssuedAt: time.Now(), + ValidityPeriods: sdk.NewAuthConsumerValidityPeriod(time.Now(), 0), Warnings: sdk.AuthConsumerWarnings{{ Type: sdk.WarningGroupInvalid, GroupID: g1.ID, @@ -233,7 +233,7 @@ func TestConsumerRemoveGroup_RemoveLastConsumerGroup(t *testing.T) { ScopeDetails: sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeAdmin), GroupIDs: []int64{g1.ID}, AuthentifiedUserID: u.ID, - IssuedAt: time.Now(), + ValidityPeriods: sdk.NewAuthConsumerValidityPeriod(time.Now(), 0), } require.NoError(t, authentication.InsertConsumer(context.TODO(), db, &c)) @@ -271,7 +271,7 @@ func TestConsumerRemoveGroup_RemoveLastInvalidConsumerGroup(t *testing.T) { ScopeDetails: sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeAdmin), InvalidGroupIDs: []int64{g1.ID}, AuthentifiedUserID: u.ID, - IssuedAt: time.Now(), + ValidityPeriods: sdk.NewAuthConsumerValidityPeriod(time.Now(), 0), Disabled: true, Warnings: sdk.AuthConsumerWarnings{ { @@ -322,7 +322,7 @@ func TestConsumerRestoreInvalidatedGroupForUser_RestoreInvalidatedGroup(t *testi GroupIDs: []int64{g2.ID}, InvalidGroupIDs: []int64{g1.ID}, AuthentifiedUserID: u.ID, - IssuedAt: time.Now(), + ValidityPeriods: sdk.NewAuthConsumerValidityPeriod(time.Now(), 0), Warnings: sdk.AuthConsumerWarnings{ { Type: sdk.WarningGroupInvalid, @@ -365,7 +365,7 @@ func TestConsumerLifecycle_RestoreInvalidatedLastGroup(t *testing.T) { ScopeDetails: sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeAdmin), InvalidGroupIDs: []int64{g1.ID}, AuthentifiedUserID: u.ID, - IssuedAt: time.Now(), + ValidityPeriods: sdk.NewAuthConsumerValidityPeriod(time.Now(), 0), Disabled: true, Warnings: sdk.AuthConsumerWarnings{ { @@ -412,7 +412,7 @@ func TestConsumerInvalidateGroupsForUser_InvalidateLastGroups(t *testing.T) { GroupIDs: []int64{g1.ID}, InvalidGroupIDs: []int64{g2.ID}, AuthentifiedUserID: u.ID, - IssuedAt: time.Now(), + ValidityPeriods: sdk.NewAuthConsumerValidityPeriod(time.Now(), 0), Warnings: sdk.AuthConsumerWarnings{ { Type: sdk.WarningGroupInvalid, @@ -462,7 +462,7 @@ func TestConsumerRestoreInvalidatedGroupsForUser(t *testing.T) { ScopeDetails: sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeAdmin), InvalidGroupIDs: []int64{g1.ID, g2.ID}, AuthentifiedUserID: u.ID, - IssuedAt: time.Now(), + ValidityPeriods: sdk.NewAuthConsumerValidityPeriod(time.Now(), 0), Disabled: true, Warnings: sdk.AuthConsumerWarnings{ { diff --git a/engine/api/authentication/dao_consumer.go b/engine/api/authentication/dao_consumer.go index 264f622ca2..f9aea60e3e 100644 --- a/engine/api/authentication/dao_consumer.go +++ b/engine/api/authentication/dao_consumer.go @@ -76,6 +76,8 @@ func getConsumer(ctx context.Context, db gorp.SqlExecutor, q gorpmapping.Query, } } + ac.ValidityPeriods.Sort() + return &ac, nil } @@ -117,6 +119,7 @@ func InsertConsumer(ctx context.Context, db gorpmapper.SqlExecutorWithTx, ac *sd ac.ID = sdk.UUID() } ac.Created = time.Now() + ac.ValidityPeriods.Sort() c := authConsumer{AuthConsumer: *ac} if err := gorpmapping.InsertAndSign(ctx, db, &c); err != nil { return sdk.WrapError(err, "unable to insert auth consumer") @@ -127,6 +130,7 @@ func InsertConsumer(ctx context.Context, db gorpmapper.SqlExecutorWithTx, ac *sd // UpdateConsumer in database. func UpdateConsumer(ctx context.Context, db gorpmapper.SqlExecutorWithTx, ac *sdk.AuthConsumer) error { + ac.ValidityPeriods.Sort() c := authConsumer{AuthConsumer: *ac} if err := gorpmapping.UpdateAndSign(ctx, db, &c); err != nil { return sdk.WrapError(err, "unable to update auth consumer with id: %s", ac.ID) @@ -140,3 +144,12 @@ func DeleteConsumerByID(db gorp.SqlExecutor, id string) error { _, err := db.Exec("DELETE FROM auth_consumer WHERE id = $1", id) return sdk.WrapError(err, "unable to delete auth consumer with id %s", id) } + +// UpdateConsumerLastAuthentication updates only the column last_authentication +func UpdateConsumerLastAuthentication(ctx context.Context, db gorp.SqlExecutor, ac *sdk.AuthConsumer) error { + c := authConsumer{AuthConsumer: *ac} + err := gorpmapping.UpdateColumns(db, &c, func(cm *gorp.ColumnMap) bool { + return cm.ColumnName == "last_authentication" + }) + return sdk.WrapError(err, "unable to update last_authentication auth consumer with id %s", ac.ID) +} diff --git a/engine/api/authentication/dao_consumer_test.go b/engine/api/authentication/dao_consumer_test.go index b137c84282..caf1753172 100644 --- a/engine/api/authentication/dao_consumer_test.go +++ b/engine/api/authentication/dao_consumer_test.go @@ -33,7 +33,7 @@ func TestLoadConsumer(t *testing.T) { ScopeDetails: sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeAdmin), GroupIDs: []int64{5, 10}, AuthentifiedUserID: u.ID, - IssuedAt: time.Now(), + ValidityPeriods: sdk.NewAuthConsumerValidityPeriod(time.Now(), 0), } require.NoError(t, authentication.InsertConsumer(context.TODO(), db, &c1)) @@ -44,7 +44,7 @@ func TestLoadConsumer(t *testing.T) { ScopeDetails: sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeAdmin), GroupIDs: []int64{10, 15}, AuthentifiedUserID: u.ID, - IssuedAt: time.Now(), + ValidityPeriods: sdk.NewAuthConsumerValidityPeriod(time.Now(), 0), } require.NoError(t, authentication.InsertConsumer(context.TODO(), db, &c2)) @@ -98,7 +98,7 @@ func TestInsertConsumer(t *testing.T) { c := sdk.AuthConsumer{ Name: sdk.RandomString(10), AuthentifiedUserID: u.ID, - IssuedAt: time.Now(), + ValidityPeriods: sdk.NewAuthConsumerValidityPeriod(time.Now(), 0), } require.NoError(t, authentication.InsertConsumer(context.TODO(), db, &c)) @@ -119,7 +119,7 @@ func TestUpdateConsumer(t *testing.T) { c := sdk.AuthConsumer{ Name: sdk.RandomString(10), AuthentifiedUserID: u.ID, - IssuedAt: time.Now(), + ValidityPeriods: sdk.NewAuthConsumerValidityPeriod(time.Now(), 0), } require.NoError(t, authentication.InsertConsumer(context.TODO(), db, &c)) @@ -142,7 +142,7 @@ func TestDeleteConsumer(t *testing.T) { c := sdk.AuthConsumer{ Name: sdk.RandomString(10), AuthentifiedUserID: u.ID, - IssuedAt: time.Now(), + ValidityPeriods: sdk.NewAuthConsumerValidityPeriod(time.Now(), 0), } require.NoError(t, authentication.InsertConsumer(context.TODO(), db, &c)) diff --git a/engine/api/authentication/loader_consumer_test.go b/engine/api/authentication/loader_consumer_test.go index b9c0f26b3c..b838322c85 100644 --- a/engine/api/authentication/loader_consumer_test.go +++ b/engine/api/authentication/loader_consumer_test.go @@ -44,7 +44,7 @@ func TestWithConsumerGroups(t *testing.T) { require.NoError(t, err) assert.NotNil(t, 0, len(localConsumer.Groups), "no group ids on local consumer so no groups are expected") - newConsumer, _, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), sdk.RandomString(10), localConsumer, + newConsumer, _, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), sdk.RandomString(10), 0, localConsumer, []int64{g1.ID, g2.ID}, sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeAccessToken)) require.NoError(t, err) builtinConsumer, err := authentication.LoadConsumerByID(context.TODO(), db, newConsumer.ID, diff --git a/engine/api/authentication/local/consumer.go b/engine/api/authentication/local/consumer.go index 05e0b44f82..5b6f310c2d 100644 --- a/engine/api/authentication/local/consumer.go +++ b/engine/api/authentication/local/consumer.go @@ -29,7 +29,7 @@ func newConsumerWithData(ctx context.Context, db gorpmapper.SqlExecutorWithTx, u Data: map[string]string{ "verified": sdk.FalseString, }, - IssuedAt: time.Now(), + ValidityPeriods: sdk.NewAuthConsumerValidityPeriod(time.Now(), 0), } for k, v := range data { diff --git a/engine/api/authentication/local/reset.go b/engine/api/authentication/local/reset.go index cd44156c35..dfdb01d45a 100644 --- a/engine/api/authentication/local/reset.go +++ b/engine/api/authentication/local/reset.go @@ -17,9 +17,10 @@ type resetLocalConsumerToken struct { // NewResetConsumerToken returns a new reset consumer token for given consumer id. func NewResetConsumerToken(store cache.Store, consumerID string) (string, error) { + var now = time.Now() payload := resetLocalConsumerToken{ ConsumerID: consumerID, - Nonce: time.Now().Unix(), + Nonce: now.Unix(), } cacheKey := cache.Key("authentication:consumer:reset", consumerID) @@ -27,7 +28,7 @@ func NewResetConsumerToken(store cache.Store, consumerID string) (string, error) return "", err } - return authentication.SignJWS(payload, resetLocalConsumerTokenDuration) + return authentication.SignJWS(payload, now, resetLocalConsumerTokenDuration) } // CheckResetConsumerToken checks that the given signature is a valid reset consumer token. diff --git a/engine/api/authentication/local/verify.go b/engine/api/authentication/local/verify.go index 786c802346..bfcbcd7d3c 100644 --- a/engine/api/authentication/local/verify.go +++ b/engine/api/authentication/local/verify.go @@ -30,7 +30,7 @@ func NewRegistrationToken(store cache.Store, regID string, isFirstConnection boo return "", err } - return authentication.SignJWS(payload, verifyLocalRegistrationTokenDuration) + return authentication.SignJWS(payload, time.Now(), verifyLocalRegistrationTokenDuration) } // CheckRegistrationToken checks that the given signature is a valid registration token. diff --git a/engine/api/authentication/signin_state.go b/engine/api/authentication/signin_state.go index cb009b1299..8f6af088a5 100644 --- a/engine/api/authentication/signin_state.go +++ b/engine/api/authentication/signin_state.go @@ -8,13 +8,14 @@ import ( // NewDefaultSigninStateToken returns a jws used for signin request. func NewDefaultSigninStateToken(origin, redirectURI string, isFirstConnection bool) (string, error) { + var now = time.Now() payload := sdk.AuthSigninConsumerToken{ Origin: origin, RedirectURI: redirectURI, - IssuedAt: time.Now().Unix(), + IssuedAt: now.Unix(), IsFirstConnection: isFirstConnection, } - return SignJWS(payload, sdk.AuthSigninConsumerTokenDuration) + return SignJWS(payload, now, sdk.AuthSigninConsumerTokenDuration) } // CheckDefaultSigninStateToken checks if a given signature is a valid signin state. diff --git a/engine/api/migrate/migrate_auth_consumer_token_expiration.go b/engine/api/migrate/migrate_auth_consumer_token_expiration.go new file mode 100644 index 0000000000..01292ef656 --- /dev/null +++ b/engine/api/migrate/migrate_auth_consumer_token_expiration.go @@ -0,0 +1,77 @@ +package migrate + +import ( + "context" + "database/sql" + "time" + + "github.com/go-gorp/gorp" + "github.com/ovh/cds/engine/api/authentication" + "github.com/ovh/cds/engine/gorpmapper" + "github.com/ovh/cds/sdk" + "github.com/rockbears/log" +) + +func AuthConsumerTokenExpiration(ctx context.Context, dbFunc func() *gorp.DbMap, duration time.Duration) error { + log.Info(ctx, "starting auth consumer token expiration migration") + defer log.Info(ctx, "ending authconsumer token expiration migration") + + var authConsumerIDs []string + _, err := dbFunc().Select(&authConsumerIDs, "select id from auth_consumer where validity_periods is null") + if err != nil && err != sql.ErrNoRows { + return sdk.WrapError(err, "unable to load auth_consumer.id") + } + + for _, id := range authConsumerIDs { + tx, err := dbFunc().Begin() + if err != nil { + ctx := sdk.ContextWithStacktrace(ctx, err) + log.Error(ctx, "unable to start transaction") + continue + } + if err := authConsumerTokenExpirationPerID(ctx, tx, id, duration); err != nil { + ctx := sdk.ContextWithStacktrace(ctx, err) + log.Error(ctx, "%v", err) + tx.Rollback() // nolint + continue + } + if err := tx.Commit(); err != nil { + ctx := sdk.ContextWithStacktrace(ctx, err) + log.Error(ctx, "unable to commit transaction") + continue + } + } + + return nil +} + +func authConsumerTokenExpirationPerID(ctx context.Context, tx gorpmapper.SqlExecutorWithTx, id string, duration time.Duration) error { + // Lock the row + id, err := tx.SelectStr("select id from auth_consumer where id=$1 and validity_periods is null for update skip locked") + if err == sql.ErrNoRows { + return nil + } + if err != nil { + return err + } + if id == "" { + return nil + } + + log.Info(ctx, "migrating consumer %s", id) + + // Load the consumer + consumer, err := authentication.LoadConsumerByID(ctx, tx, id) + if err != nil { + return err + } + + if len(consumer.ValidityPeriods) > 0 { + return nil + } + + consumer.ValidityPeriods = sdk.NewAuthConsumerValidityPeriod(time.Now(), duration) + log.Info(ctx, "consumer %q IAT=%v Expiration=%v", consumer.ID, consumer.ValidityPeriods.Latest().IssuedAt, consumer.ValidityPeriods.Latest().IssuedAt.Add(consumer.ValidityPeriods.Latest().Duration)) + + return authentication.UpdateConsumer(ctx, tx, consumer) +} diff --git a/engine/api/migrate/migration.go b/engine/api/migrate/migration.go index 5f2ef932c5..df02625c12 100644 --- a/engine/api/migrate/migration.go +++ b/engine/api/migrate/migration.go @@ -52,9 +52,10 @@ func Run(ctx context.Context, db gorp.SqlExecutor) { wg.Done() } }() - mig, errMig := GetByName(db, currentMigration.Name) - if errMig != nil { - log.Error(ctx, "Cannot get migration %s : %v", currentMigration.Name, errMig) + mig, err := GetByName(db, currentMigration.Name) + if err != nil { + ctx := sdk.ContextWithStacktrace(ctx, err) + log.Error(ctx, "Cannot get migration %s : %v", currentMigration.Name, err) return } if mig != nil { @@ -74,6 +75,7 @@ func Run(ctx context.Context, db gorp.SqlExecutor) { currentMigration.Progress = "Begin" } if err := Insert(db, ¤tMigration); err != nil { + ctx := sdk.ContextWithStacktrace(ctx, err) log.Error(ctx, "Cannot insert migration %s : %v", currentMigration.Name, err) return } @@ -85,6 +87,7 @@ func Run(ctx context.Context, db gorp.SqlExecutor) { log.Info(ctx, "Migration [%s]: begin", currentMigration.Name) if err := currentMigration.ExecFunc(contex); err != nil { + ctx := sdk.ContextWithStacktrace(ctx, err) log.Error(ctx, "migration %s in ERROR : %v", currentMigration.Name, err) currentMigration.Error = err.Error() } @@ -93,6 +96,7 @@ func Run(ctx context.Context, db gorp.SqlExecutor) { currentMigration.Status = sdk.MigrationStatusDone if err := Update(db, ¤tMigration); err != nil { + ctx := sdk.ContextWithStacktrace(ctx, err) log.Error(ctx, "Cannot update migration %s : %v", currentMigration.Name, err) } log.Info(ctx, "Migration [%s]: Done", currentMigration.Name) diff --git a/engine/api/project_test.go b/engine/api/project_test.go index 0ae3c6e2ed..9d2a07f16f 100644 --- a/engine/api/project_test.go +++ b/engine/api/project_test.go @@ -292,7 +292,7 @@ func Test_getProjectsHandler_AsProvider(t *testing.T) { localConsumer, err := authentication.LoadConsumerByTypeAndUserID(context.TODO(), api.mustDB(), sdk.ConsumerLocal, admin.ID, authentication.LoadConsumerOptions.WithAuthentifiedUser) require.NoError(t, err) - _, jws, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), sdk.RandomString(10), localConsumer, admin.GetGroupIDs(), + _, jws, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), sdk.RandomString(10), 0, localConsumer, admin.GetGroupIDs(), sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeProject)) require.NoError(t, err) @@ -322,7 +322,7 @@ func Test_getprojectsHandler_AsProviderWithRequestedUsername(t *testing.T) { localConsumer, err := authentication.LoadConsumerByTypeAndUserID(context.TODO(), api.mustDB(), sdk.ConsumerLocal, admin.ID, authentication.LoadConsumerOptions.WithAuthentifiedUser) require.NoError(t, err) - _, jws, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), sdk.RandomString(10), localConsumer, admin.GetGroupIDs(), + _, jws, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), sdk.RandomString(10), 0, localConsumer, admin.GetGroupIDs(), sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeProject)) u, _ := assets.InsertLambdaUser(t, db) @@ -434,7 +434,7 @@ func Test_getProjectsHandler_FilterByRepo(t *testing.T) { localConsumer, err := authentication.LoadConsumerByTypeAndUserID(context.TODO(), api.mustDB(), sdk.ConsumerLocal, admin.ID, authentication.LoadConsumerOptions.WithAuthentifiedUser) require.NoError(t, err) - _, jws, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), sdk.RandomString(10), localConsumer, admin.GetGroupIDs(), + _, jws, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), sdk.RandomString(10), 0, localConsumer, admin.GetGroupIDs(), sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeProject)) u, _ := assets.InsertLambdaUser(t, db) diff --git a/engine/api/router_middleware_auth_permission_test.go b/engine/api/router_middleware_auth_permission_test.go index 734d9b581c..22287d6940 100644 --- a/engine/api/router_middleware_auth_permission_test.go +++ b/engine/api/router_middleware_auth_permission_test.go @@ -201,7 +201,7 @@ func Test_checkUserPermissions(t *testing.T) { ctx := context.WithValue(context.TODO(), contextConsumer, &sdk.AuthConsumer{ AuthentifiedUserID: c.ConsumerAuthentifiedUser.ID, AuthentifiedUser: c.ConsumerAuthentifiedUser, - IssuedAt: time.Now(), + ValidityPeriods: sdk.NewAuthConsumerValidityPeriod(time.Now(), 0), }) ctx = context.WithValue(ctx, contextDriverManifest, &sdk.AuthDriverManifest{}) err := api.checkUserPermissions(ctx, &responseTracker{}, c.TargetAuthentifiedUser.Username, c.Permission, nil) @@ -284,7 +284,7 @@ func Test_checkUserPublicPermissions(t *testing.T) { ctx := context.WithValue(context.TODO(), contextConsumer, &sdk.AuthConsumer{ AuthentifiedUserID: c.ConsumerAuthentifiedUser.ID, AuthentifiedUser: c.ConsumerAuthentifiedUser, - IssuedAt: time.Now(), + ValidityPeriods: sdk.NewAuthConsumerValidityPeriod(time.Now(), 0), }) ctx = context.WithValue(ctx, contextDriverManifest, &sdk.AuthDriverManifest{}) err := api.checkUserPublicPermissions(ctx, &responseTracker{}, c.TargetAuthentifiedUser.Username, c.Permission, nil) @@ -371,7 +371,7 @@ func Test_checkConsumerPermissions(t *testing.T) { ctx := context.WithValue(context.TODO(), contextConsumer, &sdk.AuthConsumer{ AuthentifiedUserID: c.ConsumerAuthentifiedUser.ID, AuthentifiedUser: c.ConsumerAuthentifiedUser, - IssuedAt: time.Now(), + ValidityPeriods: sdk.NewAuthConsumerValidityPeriod(time.Now(), 0), }) ctx = context.WithValue(ctx, contextDriverManifest, &sdk.AuthDriverManifest{}) err := api.checkConsumerPermissions(ctx, &responseTracker{}, c.TargetConsumer.ID, c.Permission, nil) @@ -462,7 +462,7 @@ func Test_checkSessionPermissions(t *testing.T) { ctx := context.WithValue(context.TODO(), contextConsumer, &sdk.AuthConsumer{ AuthentifiedUserID: c.ConsumerAuthentifiedUser.ID, AuthentifiedUser: c.ConsumerAuthentifiedUser, - IssuedAt: time.Now(), + ValidityPeriods: sdk.NewAuthConsumerValidityPeriod(time.Now(), 0), }) ctx = context.WithValue(ctx, contextDriverManifest, &sdk.AuthDriverManifest{}) err := api.checkSessionPermissions(ctx, &responseTracker{}, c.TargetSession.ID, c.Permission, nil) diff --git a/engine/api/router_middleware_auth_test.go b/engine/api/router_middleware_auth_test.go index 440f37fa53..965d5b9e77 100644 --- a/engine/api/router_middleware_auth_test.go +++ b/engine/api/router_middleware_auth_test.go @@ -56,7 +56,7 @@ func Test_authMiddleware_WithAuthConsumerDisabled(t *testing.T) { localConsumer, err := authentication.LoadConsumerByTypeAndUserID(context.TODO(), db, sdk.ConsumerLocal, u.ID, authentication.LoadConsumerOptions.WithAuthentifiedUser) require.NoError(t, err) - builtinConsumer, _, err := builtin.NewConsumer(context.TODO(), db, "builtin", "", localConsumer, []int64{g.ID}, + builtinConsumer, _, err := builtin.NewConsumer(context.TODO(), db, "builtin", "", 0, localConsumer, []int64{g.ID}, sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopes...)) require.NoError(t, err) builtinSession, err := authentication.NewSession(context.TODO(), db, builtinConsumer, time.Second*5) @@ -198,7 +198,7 @@ func Test_authMiddleware_WithAuthConsumerScoped(t *testing.T) { localConsumer, err := authentication.LoadConsumerByTypeAndUserID(context.TODO(), db, sdk.ConsumerLocal, u.ID, authentication.LoadConsumerOptions.WithAuthentifiedUser) require.NoError(t, err) - builtinConsumer, _, err := builtin.NewConsumer(context.TODO(), db, "builtin", "", localConsumer, []int64{g.ID}, []sdk.AuthConsumerScopeDetail{ + builtinConsumer, _, err := builtin.NewConsumer(context.TODO(), db, "builtin", "", 0, localConsumer, []int64{g.ID}, []sdk.AuthConsumerScopeDetail{ { Scope: sdk.AuthConsumerScopeAction, Endpoints: sdk.AuthConsumerScopeEndpoints{ diff --git a/engine/api/services_test.go b/engine/api/services_test.go index 57a6795917..1d995ddada 100644 --- a/engine/api/services_test.go +++ b/engine/api/services_test.go @@ -29,9 +29,9 @@ func TestServicesHandlers(t *testing.T) { }) require.NotEmpty(t, uri) req := assets.NewJWTAuthentifiedRequest(t, jwtAdmin, http.MethodPost, uri, sdk.AuthConsumer{ - Name: sdk.RandomString(10), - ScopeDetails: sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeService), - IssuedAt: time.Now(), + Name: sdk.RandomString(10), + ScopeDetails: sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeService), + ValidityPeriods: sdk.NewAuthConsumerValidityPeriod(time.Now(), 0), }) rec := httptest.NewRecorder() api.Router.Mux.ServeHTTP(rec, req) diff --git a/engine/api/test/assets/assets.go b/engine/api/test/assets/assets.go index 10c88d5a4a..041405e871 100644 --- a/engine/api/test/assets/assets.go +++ b/engine/api/test/assets/assets.go @@ -461,7 +461,7 @@ func InsertHatchery(t *testing.T, db gorpmapper.SqlExecutorWithTx, grp sdk.Group consumer, err := authentication.LoadConsumerByTypeAndUserID(context.TODO(), db, sdk.ConsumerLocal, usr1.ID, authentication.LoadConsumerOptions.WithAuthentifiedUser) require.NoError(t, err) - hConsumer, _, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), "", consumer, []int64{grp.ID}, sdk.NewAuthConsumerScopeDetails( + hConsumer, _, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), "", 0, consumer, []int64{grp.ID}, sdk.NewAuthConsumerScopeDetails( sdk.AuthConsumerScopeHatchery, sdk.AuthConsumerScopeRunExecution, sdk.AuthConsumerScopeService, sdk.AuthConsumerScopeWorkerModel)) require.NoError(t, err) @@ -498,7 +498,7 @@ func InsertService(t *testing.T, db gorpmapper.SqlExecutorWithTx, name, serviceT sharedGroup, err := group.LoadByName(context.TODO(), db, sdk.SharedInfraGroupName) require.NoError(t, err) - hConsumer, _, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), "", consumer, []int64{sharedGroup.ID}, + hConsumer, _, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), "", 0, consumer, []int64{sharedGroup.ID}, sdk.NewAuthConsumerScopeDetails(append(scopes, sdk.AuthConsumerScopeProject)...)) require.NoError(t, err) @@ -529,7 +529,7 @@ func InitCDNService(t *testing.T, db gorpmapper.SqlExecutorWithTx, scopes ...sdk sharedGroup, err := group.LoadByName(context.TODO(), db, sdk.SharedInfraGroupName) require.NoError(t, err) - hConsumer, _, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), "", consumer, []int64{sharedGroup.ID}, + hConsumer, _, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), "", 0, consumer, []int64{sharedGroup.ID}, sdk.NewAuthConsumerScopeDetails(append(scopes, sdk.AuthConsumerScopeRunExecution, sdk.AuthConsumerScopeService, sdk.AuthConsumerScopeWorker)...)) require.NoError(t, err) diff --git a/engine/api/websocket_test.go b/engine/api/websocket_test.go index 4e9434b841..3788fdd09e 100644 --- a/engine/api/websocket_test.go +++ b/engine/api/websocket_test.go @@ -32,7 +32,7 @@ func Test_websocketWrongFilters(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)) chanMessageReceived := make(chan sdk.WebsocketEvent) @@ -186,7 +186,7 @@ func Test_websocketDeconnection(t *testing.T) { } require.NoError(t, workflow.Insert(context.TODO(), db, api.Cache, *proj, &w)) - _, 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)) // Open websocket diff --git a/engine/api/worker.go b/engine/api/worker.go index 16eb0622a2..5cd5d56357 100644 --- a/engine/api/worker.go +++ b/engine/api/worker.go @@ -7,6 +7,7 @@ import ( "fmt" "net/http" "strings" + "time" "github.com/go-gorp/gorp" "github.com/gorilla/mux" @@ -94,6 +95,13 @@ func (api *API) postRegisterWorkerHandler() service.Handler { ) } + // Store the last authentication date on the consumer + now := time.Now() + workerConsumer.LastAuthentication = &now + if err := authentication.UpdateConsumerLastAuthentication(ctx, tx, workerConsumer); err != nil { + return err + } + if err := tx.Commit(); err != nil { return sdk.WithStack(err) } diff --git a/engine/api/workflow_purge_test.go b/engine/api/workflow_purge_test.go index 5503d10357..8b6735a437 100644 --- a/engine/api/workflow_purge_test.go +++ b/engine/api/workflow_purge_test.go @@ -34,7 +34,7 @@ func Test_purgeDryRunHandler(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)) key := sdk.RandomString(10) diff --git a/engine/api/workflow_run_test.go b/engine/api/workflow_run_test.go index 255f3edfac..1a8579f578 100644 --- a/engine/api/workflow_run_test.go +++ b/engine/api/workflow_run_test.go @@ -1644,7 +1644,7 @@ func Test_postWorkflowRunHandlerWithoutRightConditionsOnHook(t *testing.T) { return writeError(w, err) } - for k, _ := range req { + for k := range req { req[k] = sdk.NodeHook{ Conditions: sdk.WorkflowNodeConditions{ LuaScript: "return false", diff --git a/engine/api/workflow_test.go b/engine/api/workflow_test.go index 37b2986841..bec5df80fb 100644 --- a/engine/api/workflow_test.go +++ b/engine/api/workflow_test.go @@ -381,7 +381,7 @@ func Test_getWorkflowHandler_AsProvider(t *testing.T) { localConsumer, err := authentication.LoadConsumerByTypeAndUserID(context.TODO(), api.mustDB(), sdk.ConsumerLocal, admin.ID, authentication.LoadConsumerOptions.WithAuthentifiedUser) require.NoError(t, err) - _, jws, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), sdk.RandomString(10), localConsumer, admin.GetGroupIDs(), + _, jws, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), sdk.RandomString(10), 0, localConsumer, admin.GetGroupIDs(), sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeProject)) u, _ := assets.InsertLambdaUser(t, db) @@ -1757,7 +1757,7 @@ func Test_getWorkflowsHandler_FilterByRepo(t *testing.T) { localConsumer, err := authentication.LoadConsumerByTypeAndUserID(context.TODO(), api.mustDB(), sdk.ConsumerLocal, admin.ID, authentication.LoadConsumerOptions.WithAuthentifiedUser) require.NoError(t, err) - _, jws, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), sdk.RandomString(10), localConsumer, admin.GetGroupIDs(), + _, jws, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), sdk.RandomString(10), 0, localConsumer, admin.GetGroupIDs(), sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeProject)) u, _ := assets.InsertLambdaUser(t, db) @@ -1838,7 +1838,7 @@ func Test_getSearchWorkflowHandler(t *testing.T) { localConsumer, err := authentication.LoadConsumerByTypeAndUserID(context.TODO(), api.mustDB(), sdk.ConsumerLocal, admin.ID, authentication.LoadConsumerOptions.WithAuthentifiedUser) require.NoError(t, err) - _, jws, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), sdk.RandomString(10), localConsumer, admin.GetGroupIDs(), + _, jws, err := builtin.NewConsumer(context.TODO(), db, sdk.RandomString(10), sdk.RandomString(10), 0, localConsumer, admin.GetGroupIDs(), sdk.NewAuthConsumerScopeDetails(sdk.AuthConsumerScopeProject)) u, _ := assets.InsertLambdaUser(t, db) diff --git a/engine/authentication/signature.go b/engine/authentication/signature.go index 5ca2d7797b..b2296180e6 100644 --- a/engine/authentication/signature.go +++ b/engine/authentication/signature.go @@ -20,7 +20,7 @@ type Signer interface { GetIssuerName() string GetSigningKey() *rsa.PrivateKey SignJWT(jwtToken *jwt.Token) (string, error) - SignJWS(content interface{}, duration time.Duration) (string, error) + SignJWS(content interface{}, now time.Time, duration time.Duration) (string, error) } type Verifier interface { @@ -97,7 +97,7 @@ type signaturePayload struct { } // SignJWS returns a jws string using CDS signing key. -func (s signer) SignJWS(content interface{}, duration time.Duration) (string, error) { +func (s signer) SignJWS(content interface{}, now time.Time, duration time.Duration) (string, error) { buf, err := json.Marshal(content) if err != nil { return "", sdk.WithStack(err) @@ -112,7 +112,7 @@ func (s signer) SignJWS(content interface{}, duration time.Duration) (string, er Data: jsonData, } if duration > 0 { - payload.Expire = time.Now().Add(duration).Unix() + payload.Expire = now.Add(duration).Unix() } signer, err := jws.NewSigner(s.signingKey) diff --git a/engine/cdn/storage/storageunit_test.go b/engine/cdn/storage/storageunit_test.go index c940564970..52ea903b2c 100644 --- a/engine/cdn/storage/storageunit_test.go +++ b/engine/cdn/storage/storageunit_test.go @@ -205,7 +205,7 @@ func TestDeduplicationCrossType(t *testing.T) { require.NoError(t, cdnUnits.FillWithUnknownItems(ctx, cdnUnits.Storages[0], 100)) require.NoError(t, cdnUnits.FillSyncItemChannel(ctx, cdnUnits.Storages[0], 100)) - time.Sleep(1 * time.Second) + time.Sleep(2 * time.Second) <-ctx.Done() diff --git a/engine/config.go b/engine/config.go index 840db4e6c2..6a41b884db 100644 --- a/engine/config.go +++ b/engine/config.go @@ -317,8 +317,8 @@ func configSetStartupData(conf *Configuration) (string, error) { return "", err } - iat := time.Now() - startupCfg := api.StartupConfig{IAT: iat.Unix()} + validityPediod := sdk.NewAuthConsumerValidityPeriod(time.Now(), 0) + startupCfg := api.StartupConfig{IAT: validityPediod.Latest().IssuedAt.Unix()} if err := authentication.Init("cds-api", apiPrivateKeyPEM); err != nil { return "", err @@ -367,12 +367,12 @@ func configSetStartupData(conf *Configuration) (string, error) { Type: api.StartupConfigConsumerTypeUI, } var c = sdk.AuthConsumer{ - ID: cfg.ID, - Name: cfg.Name, - Description: cfg.Description, - Type: sdk.ConsumerBuiltin, - Data: map[string]string{}, - IssuedAt: iat, + ID: cfg.ID, + Name: cfg.Name, + Description: cfg.Description, + Type: sdk.ConsumerBuiltin, + Data: map[string]string{}, + ValidityPeriods: validityPediod, } conf.UI.API.Token, err = builtin.NewSigninConsumerToken(&c) if err != nil { @@ -390,12 +390,12 @@ func configSetStartupData(conf *Configuration) (string, error) { Type: api.StartupConfigConsumerTypeHatchery, } var c = sdk.AuthConsumer{ - ID: cfg.ID, - Name: cfg.Name, - Description: cfg.Description, - Type: sdk.ConsumerBuiltin, - Data: map[string]string{}, - IssuedAt: iat, + ID: cfg.ID, + Name: cfg.Name, + Description: cfg.Description, + Type: sdk.ConsumerBuiltin, + Data: map[string]string{}, + ValidityPeriods: validityPediod, } h.Local.API.Token, err = builtin.NewSigninConsumerToken(&c) if err != nil { @@ -415,12 +415,12 @@ func configSetStartupData(conf *Configuration) (string, error) { Type: api.StartupConfigConsumerTypeHatchery, } var c = sdk.AuthConsumer{ - ID: cfg.ID, - Name: cfg.Name, - Description: cfg.Description, - Type: sdk.ConsumerBuiltin, - Data: map[string]string{}, - IssuedAt: iat, + ID: cfg.ID, + Name: cfg.Name, + Description: cfg.Description, + Type: sdk.ConsumerBuiltin, + Data: map[string]string{}, + ValidityPeriods: validityPediod, } h.Openstack.API.Token, err = builtin.NewSigninConsumerToken(&c) if err != nil { @@ -440,12 +440,12 @@ func configSetStartupData(conf *Configuration) (string, error) { Type: api.StartupConfigConsumerTypeHatchery, } var c = sdk.AuthConsumer{ - ID: cfg.ID, - Name: cfg.Name, - Description: cfg.Description, - Type: sdk.ConsumerBuiltin, - Data: map[string]string{}, - IssuedAt: iat, + ID: cfg.ID, + Name: cfg.Name, + Description: cfg.Description, + Type: sdk.ConsumerBuiltin, + Data: map[string]string{}, + ValidityPeriods: validityPediod, } h.VSphere.API.Token, err = builtin.NewSigninConsumerToken(&c) if err != nil { @@ -466,12 +466,12 @@ func configSetStartupData(conf *Configuration) (string, error) { Type: api.StartupConfigConsumerTypeHatchery, } var c = sdk.AuthConsumer{ - ID: cfg.ID, - Name: cfg.Name, - Description: cfg.Description, - Type: sdk.ConsumerBuiltin, - Data: map[string]string{}, - IssuedAt: iat, + ID: cfg.ID, + Name: cfg.Name, + Description: cfg.Description, + Type: sdk.ConsumerBuiltin, + Data: map[string]string{}, + ValidityPeriods: validityPediod, } h.Swarm.API.Token, err = builtin.NewSigninConsumerToken(&c) if err != nil { @@ -491,12 +491,12 @@ func configSetStartupData(conf *Configuration) (string, error) { Type: api.StartupConfigConsumerTypeHatchery, } var c = sdk.AuthConsumer{ - ID: cfg.ID, - Name: cfg.Name, - Description: cfg.Description, - Type: sdk.ConsumerBuiltin, - Data: map[string]string{}, - IssuedAt: iat, + ID: cfg.ID, + Name: cfg.Name, + Description: cfg.Description, + Type: sdk.ConsumerBuiltin, + Data: map[string]string{}, + ValidityPeriods: validityPediod, } conf.Hatchery.Marathon.API.Token, err = builtin.NewSigninConsumerToken(&c) if err != nil { @@ -516,12 +516,12 @@ func configSetStartupData(conf *Configuration) (string, error) { Type: api.StartupConfigConsumerTypeHatchery, } var c = sdk.AuthConsumer{ - ID: cfg.ID, - Name: cfg.Name, - Description: cfg.Description, - Type: sdk.ConsumerBuiltin, - Data: map[string]string{}, - IssuedAt: iat, + ID: cfg.ID, + Name: cfg.Name, + Description: cfg.Description, + Type: sdk.ConsumerBuiltin, + Data: map[string]string{}, + ValidityPeriods: validityPediod, } conf.Hatchery.Kubernetes.API.Token, err = builtin.NewSigninConsumerToken(&c) if err != nil { @@ -542,12 +542,12 @@ func configSetStartupData(conf *Configuration) (string, error) { Type: api.StartupConfigConsumerTypeHooks, } var c = sdk.AuthConsumer{ - ID: cfg.ID, - Name: cfg.Name, - Description: cfg.Description, - Type: sdk.ConsumerBuiltin, - Data: map[string]string{}, - IssuedAt: iat, + ID: cfg.ID, + Name: cfg.Name, + Description: cfg.Description, + Type: sdk.ConsumerBuiltin, + Data: map[string]string{}, + ValidityPeriods: validityPediod, } conf.Hooks.API.Token, err = builtin.NewSigninConsumerToken(&c) if err != nil { @@ -564,12 +564,12 @@ func configSetStartupData(conf *Configuration) (string, error) { Type: api.StartupConfigConsumerTypeRepositories, } var c = sdk.AuthConsumer{ - ID: cfg.ID, - Name: cfg.Name, - Description: cfg.Description, - Type: sdk.ConsumerBuiltin, - Data: map[string]string{}, - IssuedAt: iat, + ID: cfg.ID, + Name: cfg.Name, + Description: cfg.Description, + Type: sdk.ConsumerBuiltin, + Data: map[string]string{}, + ValidityPeriods: validityPediod, } conf.Repositories.API.Token, err = builtin.NewSigninConsumerToken(&c) if err != nil { @@ -586,12 +586,12 @@ func configSetStartupData(conf *Configuration) (string, error) { Type: api.StartupConfigConsumerTypeDBMigrate, } var c = sdk.AuthConsumer{ - ID: cfg.ID, - Name: cfg.Name, - Description: cfg.Description, - Type: sdk.ConsumerBuiltin, - Data: map[string]string{}, - IssuedAt: iat, + ID: cfg.ID, + Name: cfg.Name, + Description: cfg.Description, + Type: sdk.ConsumerBuiltin, + Data: map[string]string{}, + ValidityPeriods: validityPediod, } conf.DatabaseMigrate.API.Token, err = builtin.NewSigninConsumerToken(&c) if err != nil { @@ -608,12 +608,12 @@ func configSetStartupData(conf *Configuration) (string, error) { Type: api.StartupConfigConsumerTypeVCS, } var c = sdk.AuthConsumer{ - ID: cfg.ID, - Name: cfg.Name, - Description: cfg.Description, - Type: sdk.ConsumerBuiltin, - Data: map[string]string{}, - IssuedAt: iat, + ID: cfg.ID, + Name: cfg.Name, + Description: cfg.Description, + Type: sdk.ConsumerBuiltin, + Data: map[string]string{}, + ValidityPeriods: validityPediod, } conf.VCS.API.Token, err = builtin.NewSigninConsumerToken(&c) if err != nil { @@ -630,12 +630,12 @@ func configSetStartupData(conf *Configuration) (string, error) { Type: api.StartupConfigConsumerTypeCDN, } var c = sdk.AuthConsumer{ - ID: cfg.ID, - Name: cfg.Name, - Description: cfg.Description, - Type: sdk.ConsumerBuiltin, - Data: map[string]string{}, - IssuedAt: iat, + ID: cfg.ID, + Name: cfg.Name, + Description: cfg.Description, + Type: sdk.ConsumerBuiltin, + Data: map[string]string{}, + ValidityPeriods: validityPediod, } conf.CDN.API.Token, err = builtin.NewSigninConsumerToken(&c) if err != nil { @@ -653,12 +653,12 @@ func configSetStartupData(conf *Configuration) (string, error) { Type: api.StartupConfigConsumerTypeCDNStorageCDS, } var c = sdk.AuthConsumer{ - ID: cfg.ID, - Name: cfg.Name, - Description: cfg.Description, - Type: sdk.ConsumerBuiltin, - Data: map[string]string{}, - IssuedAt: iat, + ID: cfg.ID, + Name: cfg.Name, + Description: cfg.Description, + Type: sdk.ConsumerBuiltin, + Data: map[string]string{}, + ValidityPeriods: validityPediod, } conf.CDN.Units.Storages[k].CDS.Token, err = builtin.NewSigninConsumerToken(&c) if err != nil { @@ -677,12 +677,12 @@ func configSetStartupData(conf *Configuration) (string, error) { Type: api.StartupConfigConsumerTypeElasticsearch, } var c = sdk.AuthConsumer{ - ID: cfg.ID, - Name: cfg.Name, - Description: cfg.Description, - Type: sdk.ConsumerBuiltin, - Data: map[string]string{}, - IssuedAt: iat, + ID: cfg.ID, + Name: cfg.Name, + Description: cfg.Description, + Type: sdk.ConsumerBuiltin, + Data: map[string]string{}, + ValidityPeriods: validityPediod, } conf.ElasticSearch.API.Token, err = builtin.NewSigninConsumerToken(&c) if err != nil { @@ -691,7 +691,7 @@ func configSetStartupData(conf *Configuration) (string, error) { startupCfg.Consumers = append(startupCfg.Consumers, cfg) } - return authentication.SignJWS(startupCfg, time.Hour) + return authentication.SignJWS(startupCfg, time.Now(), time.Hour) } func getInitTokenFromExistingConfiguration(conf Configuration) (string, error) { @@ -700,7 +700,8 @@ func getInitTokenFromExistingConfiguration(conf Configuration) (string, error) { } apiPrivateKeyPEM := []byte(conf.API.Auth.RSAPrivateKey) - globalIAT := time.Now().Unix() + now := time.Now() + globalIAT := now.Unix() startupCfg := api.StartupConfig{} if err := authentication.Init("cds-api", apiPrivateKeyPEM); err != nil { @@ -951,5 +952,5 @@ func getInitTokenFromExistingConfiguration(conf Configuration) (string, error) { startupCfg.IAT = globalIAT - return authentication.SignJWS(startupCfg, time.Hour) + return authentication.SignJWS(startupCfg, now, time.Hour) } diff --git a/engine/sql/api/224_auth_token_expiration.sql b/engine/sql/api/224_auth_token_expiration.sql new file mode 100644 index 0000000000..bf0210bc98 --- /dev/null +++ b/engine/sql/api/224_auth_token_expiration.sql @@ -0,0 +1,7 @@ +-- +migrate Up +ALTER TABLE "auth_consumer" ADD COLUMN validity_periods JSONB; +ALTER TABLE "auth_consumer" ADD COLUMN last_authentication TIMESTAMP WITH TIME ZONE; + +-- +migrate Down +ALTER TABLE "auth_consumer" DROP COLUMN validity_periods; +ALTER TABLE "auth_consumer" DROP COLUMN last_authentication; diff --git a/sdk/cdsclient/client_auth.go b/sdk/cdsclient/client_auth.go index e978b4aac7..e05ee4ab9a 100644 --- a/sdk/cdsclient/client_auth.go +++ b/sdk/cdsclient/client_auth.go @@ -63,9 +63,12 @@ func (c *client) AuthConsumerDelete(username, id string) error { return err } -func (c *client) AuthConsumerRegen(username, id string) (sdk.AuthConsumerCreateResponse, error) { +func (c *client) AuthConsumerRegen(username, id string, newDuration int64, overlapDuration string) (sdk.AuthConsumerCreateResponse, error) { var consumer sdk.AuthConsumerCreateResponse - request := sdk.AuthConsumerRegenRequest{RevokeSessions: true} + request := sdk.AuthConsumerRegenRequest{ + NewDuration: newDuration, + OverlapDuration: overlapDuration, + } _, _, _, err := c.RequestJSON(context.Background(), "POST", "/user/"+username+"/auth/consumer/"+id+"/regen", request, &consumer) return consumer, err } diff --git a/sdk/cdsclient/interface.go b/sdk/cdsclient/interface.go index e376fcb3a4..96eb3756dc 100644 --- a/sdk/cdsclient/interface.go +++ b/sdk/cdsclient/interface.go @@ -585,7 +585,7 @@ type AuthClient interface { AuthConsumerSignout() error AuthConsumerListByUser(username string) (sdk.AuthConsumers, error) AuthConsumerDelete(username, id string) error - AuthConsumerRegen(username, id string) (sdk.AuthConsumerCreateResponse, error) + AuthConsumerRegen(username, id string, newDuration int64, overlapDuration string) (sdk.AuthConsumerCreateResponse, error) AuthConsumerCreateForUser(username string, request sdk.AuthConsumer) (sdk.AuthConsumerCreateResponse, error) AuthSessionListByUser(username string) (sdk.AuthSessions, error) AuthSessionDelete(username, id string) error diff --git a/sdk/cdsclient/mock_cdsclient/interface_mock.go b/sdk/cdsclient/mock_cdsclient/interface_mock.go index daaad8148e..9fb2942ece 100644 --- a/sdk/cdsclient/mock_cdsclient/interface_mock.go +++ b/sdk/cdsclient/mock_cdsclient/interface_mock.go @@ -5346,18 +5346,18 @@ func (mr *MockInterfaceMockRecorder) AuthConsumerLocalSignupVerify(token, initTo } // AuthConsumerRegen mocks base method. -func (m *MockInterface) AuthConsumerRegen(username, id string) (sdk.AuthConsumerCreateResponse, error) { +func (m *MockInterface) AuthConsumerRegen(username, id string, newDuration int64, overlapDuration string) (sdk.AuthConsumerCreateResponse, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AuthConsumerRegen", username, id) + ret := m.ctrl.Call(m, "AuthConsumerRegen", username, id, newDuration, overlapDuration) ret0, _ := ret[0].(sdk.AuthConsumerCreateResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // AuthConsumerRegen indicates an expected call of AuthConsumerRegen. -func (mr *MockInterfaceMockRecorder) AuthConsumerRegen(username, id interface{}) *gomock.Call { +func (mr *MockInterfaceMockRecorder) AuthConsumerRegen(username, id, newDuration, overlapDuration interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthConsumerRegen", reflect.TypeOf((*MockInterface)(nil).AuthConsumerRegen), username, id) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthConsumerRegen", reflect.TypeOf((*MockInterface)(nil).AuthConsumerRegen), username, id, newDuration, overlapDuration) } // AuthConsumerSignin mocks base method. @@ -10263,18 +10263,18 @@ func (mr *MockAuthClientMockRecorder) AuthConsumerLocalSignupVerify(token, initT } // AuthConsumerRegen mocks base method. -func (m *MockAuthClient) AuthConsumerRegen(username, id string) (sdk.AuthConsumerCreateResponse, error) { +func (m *MockAuthClient) AuthConsumerRegen(username, id string, newDuration int64, overlapDuration string) (sdk.AuthConsumerCreateResponse, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "AuthConsumerRegen", username, id) + ret := m.ctrl.Call(m, "AuthConsumerRegen", username, id, newDuration, overlapDuration) ret0, _ := ret[0].(sdk.AuthConsumerCreateResponse) ret1, _ := ret[1].(error) return ret0, ret1 } // AuthConsumerRegen indicates an expected call of AuthConsumerRegen. -func (mr *MockAuthClientMockRecorder) AuthConsumerRegen(username, id interface{}) *gomock.Call { +func (mr *MockAuthClientMockRecorder) AuthConsumerRegen(username, id, newDuration, overlapDuration interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthConsumerRegen", reflect.TypeOf((*MockAuthClient)(nil).AuthConsumerRegen), username, id) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AuthConsumerRegen", reflect.TypeOf((*MockAuthClient)(nil).AuthConsumerRegen), username, id, newDuration, overlapDuration) } // AuthConsumerSignin mocks base method. diff --git a/sdk/token.go b/sdk/token.go index d5d2095a8f..7fe7a9cdf6 100644 --- a/sdk/token.go +++ b/sdk/token.go @@ -4,7 +4,9 @@ import ( "context" "database/sql/driver" json "encoding/json" + "fmt" "net/http" + "sort" "time" jwt "github.com/dgrijalva/jwt-go" @@ -230,7 +232,9 @@ func (s AuthConsumerScopeSlice) Value() (driver.Value, error) { // AuthConsumerRegenRequest struct. type AuthConsumerRegenRequest struct { - RevokeSessions bool `json:"revoke_sessions"` + RevokeSessions bool `json:"revoke_sessions"` + OverlapDuration string `json:"overlap_duration"` + NewDuration int64 `json:"new_duration"` } // AuthConsumerSigninRequest struct for auth consumer signin request. @@ -382,20 +386,22 @@ type AuthConsumers []AuthConsumer // AuthConsumer issues session linked to an authentified user. type AuthConsumer struct { - ID string `json:"id" cli:"id,key" db:"id"` - Name string `json:"name" cli:"name" db:"name"` - Description string `json:"description" cli:"description" db:"description"` - ParentID *string `json:"parent_id,omitempty" db:"parent_id"` - AuthentifiedUserID string `json:"user_id,omitempty" db:"user_id"` - Type AuthConsumerType `json:"type" cli:"type" db:"type"` - Data AuthConsumerData `json:"-" db:"data"` // NEVER returns auth consumer data in json, TODO this fields should be visible only in auth package - Created time.Time `json:"created" cli:"created" db:"created"` - GroupIDs Int64Slice `json:"group_ids,omitempty" cli:"group_ids" db:"group_ids"` - InvalidGroupIDs Int64Slice `json:"invalid_group_ids,omitempty" db:"invalid_group_ids"` - ScopeDetails AuthConsumerScopeDetails `json:"scope_details,omitempty" cli:"scope_details" db:"scope_details"` - IssuedAt time.Time `json:"issued_at" cli:"issued_at" db:"issued_at"` - Disabled bool `json:"disabled" cli:"disabled" db:"disabled"` - Warnings AuthConsumerWarnings `json:"warnings,omitempty" db:"warnings"` + ID string `json:"id" cli:"id,key" db:"id"` + Name string `json:"name" cli:"name" db:"name"` + Description string `json:"description" cli:"description" db:"description"` + ParentID *string `json:"parent_id,omitempty" db:"parent_id"` + AuthentifiedUserID string `json:"user_id,omitempty" db:"user_id"` + Type AuthConsumerType `json:"type" cli:"type" db:"type"` + Data AuthConsumerData `json:"-" db:"data"` // NEVER returns auth consumer data in json, TODO this fields should be visible only in auth package + Created time.Time `json:"created" cli:"created" db:"created"` + GroupIDs Int64Slice `json:"group_ids,omitempty" cli:"group_ids" db:"group_ids"` + InvalidGroupIDs Int64Slice `json:"invalid_group_ids,omitempty" db:"invalid_group_ids"` + ScopeDetails AuthConsumerScopeDetails `json:"scope_details,omitempty" cli:"scope_details" db:"scope_details"` + DeprecatedIssuedAt time.Time `json:"issued_at" cli:"issued_at" db:"issued_at"` + Disabled bool `json:"disabled" cli:"disabled" db:"disabled"` + Warnings AuthConsumerWarnings `json:"warnings,omitempty" db:"warnings"` + LastAuthentication *time.Time `json:"last_authentication,omitempty" db:"last_authentication"` + ValidityPeriods AuthConsumerValidityPeriods `json:"validity_periods,omitempty" db:"validity_periods"` // aggregates AuthentifiedUser *AuthentifiedUser `json:"user,omitempty" db:"-"` Groups Groups `json:"groups,omitempty" db:"-"` @@ -404,6 +410,52 @@ type AuthConsumer struct { Worker *Worker `json:"-" db:"-"` } +func NewAuthConsumerValidityPeriod(iat time.Time, duration time.Duration) AuthConsumerValidityPeriods { + return AuthConsumerValidityPeriods{ + { + IssuedAt: iat, + Duration: duration, + }, + } +} + +type AuthConsumerValidityPeriods []AuthConsumerValidityPeriod + +func (p AuthConsumerValidityPeriods) Value() (driver.Value, error) { + j, err := json.Marshal(p) + return j, WrapError(err, "cannot marshal AuthConsumerValidityPeriods") +} + +func (p *AuthConsumerValidityPeriods) Scan(src interface{}) error { + if src == nil { + return nil + } + source, ok := src.([]byte) + if !ok { + return WithStack(fmt.Errorf("type assertion .([]byte) failed (%T)", src)) + } + return WrapError(json.Unmarshal(source, p), "cannot unmarshal AuthConsumerValidityPeriods") +} + +func (p AuthConsumerValidityPeriods) Latest() *AuthConsumerValidityPeriod { + if len(p) == 0 { + return nil + } + p.Sort() + return &p[0] +} + +func (p *AuthConsumerValidityPeriods) Sort() { + sort.Slice(*p, func(i, j int) bool { + return (*p)[j].IssuedAt.Before((*p)[i].IssuedAt) + }) +} + +type AuthConsumerValidityPeriod struct { + IssuedAt time.Time `json:"issued_at" cli:"issued_at" ` + Duration time.Duration `json:"duration" cli:"duration"` +} + // IsValid returns validity for auth consumer. func (c AuthConsumer) IsValid(scopeDetails AuthConsumerScopeDetails) error { if c.Name == "" { diff --git a/ui/src/app/model/authentication.model.ts b/ui/src/app/model/authentication.model.ts index a7f6986ab2..9782a620fc 100644 --- a/ui/src/app/model/authentication.model.ts +++ b/ui/src/app/model/authentication.model.ts @@ -1,4 +1,5 @@ import { WithKey } from 'app/shared/table/data-table.component'; +import * as moment from 'moment'; import { Group } from './group.model'; import { AuthentifiedUser } from './user.model'; @@ -7,6 +8,11 @@ export class AuthConsumerScopeDetail { endpoints: Array; } +export class AuthConsumerValidityPeriod { + duration: number; + issued_at: string; +} + export class AuthConsumerScopeEndpoint { route: string; methods: Array; @@ -81,6 +87,8 @@ export class AuthConsumer { groups: Array; disabled: boolean; warnings: Array; + validity_periods: Array; + last_authentication: string; // UI fields parent: AuthConsumer; diff --git a/ui/src/app/views/settings/user/consumer-details-modal/consumer-details-modal.component.ts b/ui/src/app/views/settings/user/consumer-details-modal/consumer-details-modal.component.ts index 87a41a9cd4..9900f789df 100644 --- a/ui/src/app/views/settings/user/consumer-details-modal/consumer-details-modal.component.ts +++ b/ui/src/app/views/settings/user/consumer-details-modal/consumer-details-modal.component.ts @@ -10,7 +10,7 @@ import { import { TranslateService } from '@ngx-translate/core'; import { Store } from '@ngxs/store'; import { ModalTemplate, SuiActiveModal, SuiModalService, TemplateModalConfig } from '@richardlt/ng2-semantic-ui'; -import { AuthConsumer, AuthSession } from 'app/model/authentication.model'; +import { AuthConsumer, AuthConsumerValidityPeriod, AuthSession } from 'app/model/authentication.model'; import { Group } from 'app/model/group.model'; import { AuthentifiedUser, AuthSummary } from 'app/model/user.model'; import { AuthenticationService } from 'app/service/authentication/authentication.service'; @@ -19,6 +19,7 @@ import { Item } from 'app/shared/menu/menu.component'; import { Column, ColumnType, Filter } from 'app/shared/table/data-table.component'; import { ToastService } from 'app/shared/toast/ToastService'; import { AuthenticationState } from 'app/store/authentication.state'; +import * as moment from 'moment'; export enum CloseEventType { CHILD_DETAILS = 'CHILD_DETAILS', @@ -67,6 +68,7 @@ export class ConsumerDetailsModalComponent { consumerDeletedOrDetached: boolean; regenConsumerSigninToken: string; warningText: string; + columnsValidityPeriods: Array>; constructor( private _modalService: SuiModalService, @@ -122,9 +124,7 @@ export class ConsumerDetailsModalComponent { class: 'two right aligned', selector: (c: AuthConsumer) => ({ title: 'common_details', - click: () => { - this.clickConsumerDetails(c) -} + click: () => this.clickConsumerDetails(c) }) } ]; @@ -166,6 +166,35 @@ export class ConsumerDetailsModalComponent { selector: (s: AuthSession) => s.expire_at } ]; + + this.columnsValidityPeriods = [ + >{ + type: ColumnType.DATE, + name: 'Issued at', + selector: (s: AuthConsumerValidityPeriod) => moment(s.issued_at).format() + }, + >{ + type: ColumnType.TEXT_LABELS, + name: 'Duration', + selector: (s: AuthConsumerValidityPeriod) => { + let labels = []; + if (s.duration === 0) { + return {value: '-', labels}; + } + + let ms = (s.duration / 1000000); + let days = Math.floor(ms / (1000 * 60 * 60 * 24)); + let unit = ' days'; + if (days <= 1) { + unit = ' day'; + } + return { + value: days + unit, + labels + }; + } + } + ]; } show() { @@ -257,6 +286,12 @@ export class ConsumerDetailsModalComponent { key: 'children' }); } + if (this.consumer.validity_periods.length > 0) { + this.menuItems.push({ + translate: 'validity_periods', + key: 'validity_periods' + }); + } this._cd.markForCheck(); } @@ -290,7 +325,6 @@ export class ConsumerDetailsModalComponent { this.consumerDeletedOrDetached = true; this.loading = false; this.modal.approve(true); - }); } diff --git a/ui/src/app/views/settings/user/consumer-details-modal/consumer-details-modal.html b/ui/src/app/views/settings/user/consumer-details-modal/consumer-details-modal.html index 8801c3669c..0eeeb72643 100644 --- a/ui/src/app/views/settings/user/consumer-details-modal/consumer-details-modal.html +++ b/ui/src/app/views/settings/user/consumer-details-modal/consumer-details-modal.html @@ -32,6 +32,12 @@ +
+
+ + {{consumer.last_authentication}} +
+
@@ -44,6 +50,11 @@ [data]="[consumer.parent]"> + + + + diff --git a/ui/src/app/views/settings/user/edit/user.edit.component.ts b/ui/src/app/views/settings/user/edit/user.edit.component.ts index 71a74066cd..423cac9dfe 100644 --- a/ui/src/app/views/settings/user/edit/user.edit.component.ts +++ b/ui/src/app/views/settings/user/edit/user.edit.component.ts @@ -14,6 +14,7 @@ import { Item } from 'app/shared/menu/menu.component'; import { Column, ColumnType, Filter } from 'app/shared/table/data-table.component'; import { ToastService } from 'app/shared/toast/ToastService'; import { AuthenticationState } from 'app/store/authentication.state'; +import * as moment from 'moment'; import { forkJoin } from 'rxjs'; import { finalize } from 'rxjs/operators'; import { CloseEventType, ConsumerCreateModalComponent } from '../consumer-create-modal/consumer-create-modal.component'; @@ -208,6 +209,39 @@ export class UserEditComponent implements OnInit { } } }, + >{ + name: 'End of token validity', + selector: (c: AuthConsumer) => { + if (!c.validity_periods) { + return ''; + } + + c.validity_periods.sort((x, y) => { + let dX = moment(x.issued_at).toDate(); + let dY = moment(y.issued_at).toDate(); + return dY.getTime() - dX.getTime(); + }); + + let period = c.validity_periods[0]; + if (period.duration === 0) { + return ''; + } + + let d = moment(period.issued_at).toDate(); + d.setTime(d.getTime() + (period.duration / 1000000)); + + return moment(d).fromNow(); + } + }, + >{ + name: 'Last authentication', + selector: (c: AuthConsumer) => { + if (!c.last_authentication) { + return 'never'; + } + return moment(c.last_authentication).fromNow(); + } + }, >{ type: ColumnType.BUTTON, name: 'common_action', diff --git a/ui/src/assets/i18n/en.json b/ui/src/assets/i18n/en.json index 8a8bf7ede7..d29c607a6c 100644 --- a/ui/src/assets/i18n/en.json +++ b/ui/src/assets/i18n/en.json @@ -695,6 +695,7 @@ "auth_consumer_details_modal_title": "Details for consumer '{{name}}'", "auth_consumer_create_modal_title": "Create a new consumer", "auth_consumer_create_modal_info_groups": "Let groups selection empty to create consumer with wildcard access on groups.", + "validity_periods": "Validity periods", "vcs_connection": "Connection:", "vcs_user": "User: ", "vcs_password": "Password: ",