From c8ce82d197f82069592081fc57614c9bba1879d8 Mon Sep 17 00:00:00 2001 From: Johannes Batzill Date: Wed, 26 Jul 2023 01:03:03 +0000 Subject: [PATCH] [Standalone] Make Token Expiration Optional & Fix CLI Register + Login (#239) --- cli/operations/account/login.go | 13 +++++-- cli/operations/account/register.go | 15 ++++++-- cli/operations/user/create_pat.go | 10 ++++-- cli/textui/input.go | 34 ++++++++++++------- client/client.go | 24 +++---------- client/interface.go | 4 +-- .../controller/serviceaccount/create_token.go | 4 +-- .../controller/user/create_access_token.go | 4 +-- internal/token/jwt.go | 7 +++- internal/token/token.go | 18 ++++++---- types/check/token.go | 15 ++++++-- types/token.go | 18 +++++----- 12 files changed, 104 insertions(+), 62 deletions(-) diff --git a/cli/operations/account/login.go b/cli/operations/account/login.go index fd78a5377..799758f0d 100644 --- a/cli/operations/account/login.go +++ b/cli/operations/account/login.go @@ -10,6 +10,7 @@ import ( "github.com/harness/gitness/cli/provide" "github.com/harness/gitness/cli/textui" + "github.com/harness/gitness/internal/api/controller/user" "gopkg.in/alecthomas/kingpin.v2" ) @@ -21,19 +22,25 @@ type loginCommand struct { func (c *loginCommand) run(*kingpin.ParseContext) error { ss := provide.NewSession() - username, password := textui.Credentials() + loginIdentifier, password := textui.Credentials() ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - ts, err := provide.OpenClient(c.server).Login(ctx, username, password) + in := &user.LoginInput{ + LoginIdentifier: loginIdentifier, + Password: password, + } + + ts, err := provide.OpenClient(c.server).Login(ctx, in) if err != nil { return err } return ss. SetURI(c.server). - SetExpiresAt(ts.Token.ExpiresAt). + // login token always has an expiry date + SetExpiresAt(*ts.Token.ExpiresAt). SetAccessToken(ts.AccessToken). Store() } diff --git a/cli/operations/account/register.go b/cli/operations/account/register.go index e9941ee71..e3343abb4 100644 --- a/cli/operations/account/register.go +++ b/cli/operations/account/register.go @@ -11,6 +11,7 @@ import ( "github.com/harness/gitness/cli/provide" "github.com/harness/gitness/cli/session" "github.com/harness/gitness/cli/textui" + "github.com/harness/gitness/internal/api/controller/user" "gopkg.in/alecthomas/kingpin.v2" ) @@ -30,18 +31,26 @@ type registerCommand struct { func (c *registerCommand) run(*kingpin.ParseContext) error { ss := provide.NewSession() - username, name, email, password := textui.Registration() + uid, displayName, email, password := textui.Registration() ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - ts, err := provide.OpenClient(c.server).Register(ctx, username, name, email, password) + input := &user.RegisterInput{ + UID: uid, + Email: email, + DisplayName: displayName, + Password: password, + } + + ts, err := provide.OpenClient(c.server).Register(ctx, input) if err != nil { return err } return ss. SetURI(c.server). - SetExpiresAt(ts.Token.ExpiresAt). + // register token always has an expiry date + SetExpiresAt(*ts.Token.ExpiresAt). SetAccessToken(ts.AccessToken). Store() } diff --git a/cli/operations/user/create_pat.go b/cli/operations/user/create_pat.go index 94bea35e8..f9875bff2 100644 --- a/cli/operations/user/create_pat.go +++ b/cli/operations/user/create_pat.go @@ -16,6 +16,7 @@ import ( "github.com/harness/gitness/types/enum" "github.com/drone/funcmap" + "github.com/gotidy/ptr" "gopkg.in/alecthomas/kingpin.v2" ) @@ -38,9 +39,14 @@ func (c *createPATCommand) run(*kingpin.ParseContext) error { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() + var lifeTime *time.Duration + if c.lifetimeInS > 0 { + lifeTime = ptr.Duration(time.Duration(int64(time.Second) * c.lifetimeInS)) + } + in := user.CreateTokenInput{ UID: c.uid, - Lifetime: time.Duration(int64(time.Second) * c.lifetimeInS), + Lifetime: lifeTime, Grants: enum.AccessGrantAll, } @@ -71,7 +77,7 @@ func registerCreatePAT(app *kingpin.CmdClause) { Required().StringVar(&c.uid) cmd.Arg("lifetime", "the lifetime of the token in seconds"). - Required().Int64Var(&c.lifetimeInS) + Int64Var(&c.lifetimeInS) cmd.Flag("json", "json encode the output"). BoolVar(&c.json) diff --git a/cli/textui/input.go b/cli/textui/input.go index 1aa93be8f..b6491ecbd 100644 --- a/cli/textui/input.go +++ b/cli/textui/input.go @@ -14,31 +14,41 @@ import ( "golang.org/x/term" ) -// Registration returns the username, name, email and password from stdin. +// Registration returns the userID, displayName, email and password from stdin. func Registration() (string, string, string, string) { - return Username(), Name(), Email(), Password() + return UserID(), DisplayName(), Email(), Password() } -// Credentials returns the username and password from stdin. +// Credentials returns the login identifier and password from stdin. func Credentials() (string, string) { - return Username(), Password() + return LoginIdentifier(), Password() } -// Username returns the username from stdin. -func Username() string { +// UserID returns the user ID from stdin. +func UserID() string { reader := bufio.NewReader(os.Stdin) - fmt.Print("Enter Username: ") - username, _ := reader.ReadString('\n') + fmt.Print("Enter User ID: ") + uid, _ := reader.ReadString('\n') - return strings.TrimSpace(username) + return strings.TrimSpace(uid) } -// Name returns the name from stdin. -func Name() string { +// LoginIdentifier returns the login identifier from stdin. +func LoginIdentifier() string { reader := bufio.NewReader(os.Stdin) - fmt.Print("Enter Name: ") + fmt.Print("Enter User ID or Email: ") + id, _ := reader.ReadString('\n') + + return strings.TrimSpace(id) +} + +// DisplayName returns the display name from stdin. +func DisplayName() string { + reader := bufio.NewReader(os.Stdin) + + fmt.Print("Enter Display Name: ") name, _ := reader.ReadString('\n') return strings.TrimSpace(name) diff --git a/client/client.go b/client/client.go index 2dc057a3f..6b3ec24c4 100644 --- a/client/client.go +++ b/client/client.go @@ -59,27 +59,18 @@ func (c *HTTPClient) SetDebug(debug bool) { } // Login authenticates the user and returns a JWT token. -func (c *HTTPClient) Login(ctx context.Context, username, password string) (*types.TokenResponse, error) { - form := &url.Values{} - form.Add("username", username) - form.Add("password", password) +func (c *HTTPClient) Login(ctx context.Context, input *user.LoginInput) (*types.TokenResponse, error) { out := new(types.TokenResponse) uri := fmt.Sprintf("%s/api/v1/login", c.base) - err := c.post(ctx, uri, true, form, out) + err := c.post(ctx, uri, true, input, out) return out, err } // Register registers a new user and returns a JWT token. -func (c *HTTPClient) Register(ctx context.Context, - username, displayName, email, password string) (*types.TokenResponse, error) { - form := &url.Values{} - form.Add("username", username) - form.Add("displayname", displayName) - form.Add("email", email) - form.Add("password", password) +func (c *HTTPClient) Register(ctx context.Context, input *user.RegisterInput) (*types.TokenResponse, error) { out := new(types.TokenResponse) uri := fmt.Sprintf("%s/api/v1/register", c.base) - err := c.post(ctx, uri, true, form, out) + err := c.post(ctx, uri, true, input, out) return out, err } @@ -201,12 +192,7 @@ func (c *HTTPClient) stream(ctx context.Context, rawurl, method string, noToken var buf io.ReadWriter if in != nil { buf = &bytes.Buffer{} - // if posting form data, encode the form values. - if form, ok := in.(*url.Values); ok { - if _, err = io.WriteString(buf, form.Encode()); err != nil { - log.Err(err).Msg("in stream method") - } - } else if err = json.NewEncoder(buf).Encode(in); err != nil { + if err = json.NewEncoder(buf).Encode(in); err != nil { return nil, err } } diff --git a/client/interface.go b/client/interface.go index 726ea185b..9c0b97002 100644 --- a/client/interface.go +++ b/client/interface.go @@ -14,10 +14,10 @@ import ( // Client to access the remote APIs. type Client interface { // Login authenticates the user and returns a JWT token. - Login(ctx context.Context, username, password string) (*types.TokenResponse, error) + Login(ctx context.Context, input *user.LoginInput) (*types.TokenResponse, error) // Register registers a new user and returns a JWT token. - Register(ctx context.Context, username, name, email, password string) (*types.TokenResponse, error) + Register(ctx context.Context, input *user.RegisterInput) (*types.TokenResponse, error) // Self returns the currently authenticated user. Self(ctx context.Context) (*types.User, error) diff --git a/internal/api/controller/serviceaccount/create_token.go b/internal/api/controller/serviceaccount/create_token.go index dfa3ebd4c..46636de00 100644 --- a/internal/api/controller/serviceaccount/create_token.go +++ b/internal/api/controller/serviceaccount/create_token.go @@ -18,7 +18,7 @@ import ( type CreateTokenInput struct { UID string `json:"uid"` - Lifetime time.Duration `json:"lifetime"` + Lifetime *time.Duration `json:"lifetime"` Grants enum.AccessGrant `json:"grants"` } @@ -33,7 +33,7 @@ func (c *Controller) CreateToken(ctx context.Context, session *auth.Session, if err = check.UID(in.UID); err != nil { return nil, err } - if err = check.TokenLifetime(in.Lifetime); err != nil { + if err = check.TokenLifetime(in.Lifetime, true); err != nil { return nil, err } // TODO: Added to unblock UI - Depending on product decision enforce grants, or remove Grants completely. diff --git a/internal/api/controller/user/create_access_token.go b/internal/api/controller/user/create_access_token.go index 2d69762b3..5738452f4 100644 --- a/internal/api/controller/user/create_access_token.go +++ b/internal/api/controller/user/create_access_token.go @@ -18,7 +18,7 @@ import ( type CreateTokenInput struct { UID string `json:"uid"` - Lifetime time.Duration `json:"lifetime"` + Lifetime *time.Duration `json:"lifetime"` Grants enum.AccessGrant `json:"grants"` } @@ -40,7 +40,7 @@ func (c *Controller) CreateAccessToken(ctx context.Context, session *auth.Sessio if err = check.UID(in.UID); err != nil { return nil, err } - if err = check.TokenLifetime(in.Lifetime); err != nil { + if err = check.TokenLifetime(in.Lifetime, true); err != nil { return nil, err } // TODO: Added to unblock UI - Depending on product decision enforce grants, or remove Grants completely. diff --git a/internal/token/jwt.go b/internal/token/jwt.go index 86e465350..db49243a3 100644 --- a/internal/token/jwt.go +++ b/internal/token/jwt.go @@ -27,12 +27,17 @@ type JWTClaims struct { // GenerateJWTForToken generates a jwt for a given token. func GenerateJWTForToken(token *types.Token, secret string) (string, error) { + var expiresAt int64 + if token.ExpiresAt != nil { + expiresAt = *token.ExpiresAt + } + jwtToken := jwt.NewWithClaims(jwt.SigningMethodHS256, JWTClaims{ jwt.StandardClaims{ Issuer: issuer, // times required to be in sec not millisec IssuedAt: token.IssuedAt / 1000, - ExpiresAt: token.ExpiresAt / 1000, + ExpiresAt: expiresAt / 1000, }, token.Type, token.ID, diff --git a/internal/token/token.go b/internal/token/token.go index 401f41a55..b415adf28 100644 --- a/internal/token/token.go +++ b/internal/token/token.go @@ -12,6 +12,8 @@ import ( "github.com/harness/gitness/internal/store" "github.com/harness/gitness/types" "github.com/harness/gitness/types/enum" + + "github.com/gotidy/ptr" ) const ( @@ -28,14 +30,14 @@ func CreateUserSession(ctx context.Context, tokenStore store.TokenStore, principal, principal, uid, - userTokenLifeTime, + ptr.Duration(userTokenLifeTime), enum.AccessGrantAll, ) } func CreatePAT(ctx context.Context, tokenStore store.TokenStore, createdBy *types.Principal, createdFor *types.User, - uid string, lifetime time.Duration, grants enum.AccessGrant) (*types.Token, string, error) { + uid string, lifetime *time.Duration, grants enum.AccessGrant) (*types.Token, string, error) { return Create( ctx, tokenStore, @@ -50,7 +52,7 @@ func CreatePAT(ctx context.Context, tokenStore store.TokenStore, func CreateSAT(ctx context.Context, tokenStore store.TokenStore, createdBy *types.Principal, createdFor *types.ServiceAccount, - uid string, lifetime time.Duration, grants enum.AccessGrant) (*types.Token, string, error) { + uid string, lifetime *time.Duration, grants enum.AccessGrant) (*types.Token, string, error) { return Create( ctx, tokenStore, @@ -65,9 +67,13 @@ func CreateSAT(ctx context.Context, tokenStore store.TokenStore, func Create(ctx context.Context, tokenStore store.TokenStore, tokenType enum.TokenType, createdBy *types.Principal, createdFor *types.Principal, - uid string, lifetime time.Duration, grants enum.AccessGrant) (*types.Token, string, error) { + uid string, lifetime *time.Duration, grants enum.AccessGrant) (*types.Token, string, error) { issuedAt := time.Now() - expiresAt := issuedAt.Add(lifetime) + + var expiresAt *int64 + if lifetime != nil { + expiresAt = ptr.Int64(issuedAt.Add(*lifetime).UnixMilli()) + } // create db entry first so we get the id. token := types.Token{ @@ -75,7 +81,7 @@ func Create(ctx context.Context, tokenStore store.TokenStore, UID: uid, PrincipalID: createdFor.ID, IssuedAt: issuedAt.UnixMilli(), - ExpiresAt: expiresAt.UnixMilli(), + ExpiresAt: expiresAt, Grants: grants, CreatedBy: createdBy.ID, } diff --git a/types/check/token.go b/types/check/token.go index 6621e4a97..a370f8be7 100644 --- a/types/check/token.go +++ b/types/check/token.go @@ -17,11 +17,22 @@ var ( ErrTokenLifeTimeOutOfBounds = &ValidationError{ "The life time of a token has to be between 1 day and 365 days.", } + ErrTokenLifeTimeRequired = &ValidationError{ + "The life time of a token is required.", + } ) // TokenLifetime returns true if the lifetime is valid for a token. -func TokenLifetime(lifetime time.Duration) error { - if lifetime < minTokenLifeTime || lifetime > maxTokenLifeTime { +func TokenLifetime(lifetime *time.Duration, optional bool) error { + if lifetime == nil && !optional { + return ErrTokenLifeTimeRequired + } + + if lifetime == nil { + return nil + } + + if *lifetime < minTokenLifeTime || *lifetime > maxTokenLifeTime { return ErrTokenLifeTimeOutOfBounds } diff --git a/types/token.go b/types/token.go index c73447cd6..80b65dfac 100644 --- a/types/token.go +++ b/types/token.go @@ -11,14 +11,16 @@ import ( // Represents server side infos stored for tokens we distribute. type Token struct { // TODO: int64 ID doesn't match DB - ID int64 `db:"token_id" json:"-"` - PrincipalID int64 `db:"token_principal_id" json:"principal_id"` - Type enum.TokenType `db:"token_type" json:"type"` - UID string `db:"token_uid" json:"uid"` - ExpiresAt int64 `db:"token_expires_at" json:"expires_at"` - IssuedAt int64 `db:"token_issued_at" json:"issued_at"` - Grants enum.AccessGrant `db:"token_grants" json:"grants"` - CreatedBy int64 `db:"token_created_by" json:"created_by"` + ID int64 `db:"token_id" json:"-"` + PrincipalID int64 `db:"token_principal_id" json:"principal_id"` + Type enum.TokenType `db:"token_type" json:"type"` + UID string `db:"token_uid" json:"uid"` + // ExpiresAt is an optional unix time that if specified restricts the validity of a token. + ExpiresAt *int64 `db:"token_expires_at" json:"expires_at,omitempty"` + // IssuedAt is the unix time at which the token was issued. + IssuedAt int64 `db:"token_issued_at" json:"issued_at"` + Grants enum.AccessGrant `db:"token_grants" json:"grants"` + CreatedBy int64 `db:"token_created_by" json:"created_by"` } // TokenResponse is returned as part of token creation for PAT / SAT / User Session.