[Standalone] Make Token Expiration Optional & Fix CLI Register + Login (#239)

This commit is contained in:
Johannes Batzill 2023-07-26 01:03:03 +00:00 committed by Harness
parent ef9a0f28d4
commit c8ce82d197
12 changed files with 104 additions and 62 deletions

View File

@ -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()
}

View File

@ -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()
}

View File

@ -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)

View File

@ -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)

View File

@ -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
}
}

View File

@ -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)

View File

@ -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.

View File

@ -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.

View File

@ -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,

View File

@ -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,
}

View File

@ -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
}

View File

@ -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.