Skip to content

Commit

Permalink
feat(api, cli, tests): JWT access tokens management with CLI (#3987)
Browse files Browse the repository at this point in the history
  • Loading branch information
fsamin authored and richardlt committed Mar 4, 2019
1 parent b6f202c commit 4d81bfc
Show file tree
Hide file tree
Showing 58 changed files with 9,137 additions and 122 deletions.
24 changes: 24 additions & 0 deletions cli/ask_confirm.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,30 @@ func MultiChoice(s string, opts ...string) int {
return 0
}

// MultiSelect for multiple choices question. It returns the selected options
func MultiSelect(s string, opts ...string) []int {
var result []string

if err := survey.AskOne(&survey.MultiSelect{
Message: s,
Options: opts,
PageSize: 10,
}, &result, nil); err != nil {
log.Fatal(err)
}

var choices []int
for i := range opts {
for j := range result {
if opts[i] == result[j] {
choices = append(choices, i)
}
}
}

return choices
}

// AskValueChoice ask for a string and returns it.
func AskValueChoice(s string) string {
var result string
Expand Down
268 changes: 268 additions & 0 deletions cli/cdsctl/access_token.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,268 @@
package main

import (
"errors"
"fmt"
"regexp"
"strconv"
"strings"
"time"

"github.com/spf13/cobra"

"github.com/ovh/cds/cli"
"github.com/ovh/cds/sdk"
)

func accesstoken() *cobra.Command {

var (
cmd = cli.Command{
Name: "xtoken",
Short: "Manage CDS access tokens [EXPERIMENTAL]",
}

listbyUserCmd = cli.Command{
Name: "list",
Short: "List your access tokens",
Flags: []cli.Flag{
{
Name: "group",
Type: cli.FlagSlice,
ShortHand: "g",
Usage: "filter by group",
},
},
}

newCmd = cli.Command{
Name: "new",
Short: "Create a new access token",
Flags: []cli.Flag{
{
Name: "description",
ShortHand: "d",
Usage: "what is the purpose of this token",
}, {
Name: "expiration",
ShortHand: "e",
Usage: "expiration delay of the token (1d, 24h, 1440m, 86400s)",
Default: "1d",
IsValid: func(s string) bool {
return true
},
}, {
Name: "group",
Type: cli.FlagSlice,
ShortHand: "g",
Usage: "define the scope of the token through groups",
},
},
}

regenCmd = cli.Command{
Name: "regen",
Short: "Regenerate access token",
VariadicArgs: cli.Arg{
Name: "token-id",
AllowEmpty: false,
},
}

deleteCmd = cli.Command{
Name: "delete",
Short: "Delete access token",
VariadicArgs: cli.Arg{
Name: "token-id",
AllowEmpty: true,
},
}
)

return cli.NewCommand(cmd, nil,
cli.SubCommands{
cli.NewListCommand(listbyUserCmd, accesstokenListRun, nil),
cli.NewCommand(newCmd, accesstokenNewRun, nil),
cli.NewCommand(regenCmd, accesstokenRegenRun, nil),
cli.NewCommand(deleteCmd, accesstokenDeleteRun, nil),
},
)
}

func accesstokenListRun(v cli.Values) (cli.ListResult, error) {

type displayToken struct {
ID string `cli:"id,key"`
Description string `cli:"description"`
UserName string `cli:"user"`
ExpireAt string `cli:"expired_at"`
Created string `cli:"created"`
Status string `cli:"status"`
Scope string `cli:"scope"`
}

var displayTokenFunc = func(t sdk.AccessToken) displayToken {
var groupNames []string
for _, g := range t.Groups {
groupNames = append(groupNames, g.Name)
}
return displayToken{
ID: t.ID,
Description: t.Description,
UserName: t.User.Fullname,
ExpireAt: t.ExpireAt.Format(time.RFC850),
Created: t.Created.Format(time.RFC850),
Status: t.Status,
Scope: strings.Join(groupNames, ","),
}
}

var displayAllTokensFunc = func(ts []sdk.AccessToken) []displayToken {
var res = make([]displayToken, len(ts))
for i := range ts {
res[i] = displayTokenFunc(ts[i])
}
return res
}

groups := v.GetStringSlice("group")
if len(groups) == 0 {
tokens, err := client.AccessTokenListByUser(cfg.User)
if err != nil {
return nil, err
}
return cli.AsListResult(displayAllTokensFunc(tokens)), nil
}

tokens, err := client.AccessTokenListByGroup(groups...)
if err != nil {
return nil, err
}
return cli.AsListResult(displayAllTokensFunc(tokens)), nil
}

func accesstokenNewRun(v cli.Values) error {
allGroups, err := client.GroupList()
if err != nil {
return err
}

description := v.GetString("description")
expiration := v.GetString("expiration")
groups := v.GetStringSlice("group")

// If the flag has not been set, ask interactively
if description == "" {
description = cli.AskValueChoice("Description")
}
if expiration == "" {
expiration = cli.AskValueChoice("Expiration")
}
if len(groups) == 0 {
var groupNames []string
for _, g := range allGroups {
groupNames = append(groupNames, g.Name)
}
choices := cli.MultiSelect("Groups", groupNames...)
for _, choice := range choices {
groups = append(groups, groupNames[choice])
}
}

// Compute expiration string
var r = regexp.MustCompile("([0-9])(s|m|h|d)")
if !r.MatchString(expiration) {
return errors.New("unsupported expiration expression")
}

matches := r.FindStringSubmatch(expiration)
factor, _ := strconv.ParseFloat(matches[1], 64)
unit := time.Second
switch matches[2] {
case "m":
unit = time.Minute
case "h":
unit = time.Hour
case "d":
unit = 24 * time.Hour
}

expirationDuration := time.Duration(factor) * unit

// Retrieve group IDs from all the groups accessible by the user
var groupsIDs []int64
for _, group := range groups {
var groupFound bool
for _, knowGroup := range allGroups {
if knowGroup.Name == group {
groupFound = true
groupsIDs = append(groupsIDs, knowGroup.ID)
break
}
}
if !groupFound {
return errors.New("group not found")
}
}

var request = sdk.AccessTokenRequest{
Description: description,
ExpirationDelaySecond: expirationDuration.Seconds(),
GroupsIDs: groupsIDs,
Origin: "cdsctl",
}

t, jwt, err := client.AccessTokenCreate(request)
if err != nil {
return fmt.Errorf("unable to create access token: %v", err)
}
fmt.Println()

displayToken(t, jwt)

return nil
}

func displayToken(t sdk.AccessToken, jwt string) {
fmt.Println("Token successfully generated")
fmt.Println(cli.Cyan("ID"), "\t\t", t.ID)
fmt.Println(cli.Cyan("Description"), "\t", t.Description)
fmt.Println(cli.Cyan("Creation"), "\t", t.Created.Format(time.RFC850))
fmt.Println(cli.Red("Expiration"), "\t", cli.Red(t.ExpireAt.Format(time.RFC850)))
fmt.Println(cli.Cyan("User"), "\t\t", t.User.Fullname)
var groupNames []string
for _, g := range t.Groups {
groupNames = append(groupNames, g.Name)
}
fmt.Println(cli.Cyan("Scope"), "\t\t", groupNames)
fmt.Println()
fmt.Println(cli.Red("Here it is, keep it in a safe place, it will never ne displayed again."))
fmt.Println(jwt)
}

func accesstokenRegenRun(v cli.Values) error {
tokenIDs := v.GetStringSlice("token-id")
for _, id := range tokenIDs {

t, jwt, err := client.AccessTokenRegen(id)
if err != nil {
fmt.Println("unable to regen token", id, cli.Red(err.Error()))
}

displayToken(t, jwt)
}

return nil
}

func accesstokenDeleteRun(v cli.Values) error {
tokenIDs := v.GetStringSlice("token-id")
for _, id := range tokenIDs {
if err := client.AccessTokenDelete(id); err != nil {
fmt.Println("unable to delete token", id, cli.Red(err.Error()))
}

}

return nil
}
17 changes: 15 additions & 2 deletions cli/cdsctl/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import (
"path"
"strconv"

repo "github.com/fsamin/go-repo"
"github.com/dgrijalva/jwt-go"
"github.com/fsamin/go-repo"

"github.com/ovh/cds/cli"
"github.com/ovh/cds/sdk"
Expand Down Expand Up @@ -104,11 +105,23 @@ func loadConfig(configFile string) (*cdsclient.Config, error) {
conf := &cdsclient.Config{
Host: c.Host,
User: c.User,
Token: c.Token,
Verbose: verbose,
InsecureSkipVerifyTLS: c.InsecureSkipVerifyTLS,
}

// TEMPORARY CODE
// Try to parse the token as JWT and set it as access token
if _, _, err := new(jwt.Parser).ParseUnverified(c.Token, &sdk.AccessTokenJWTClaims{}); err == nil {
conf.AccessToken = c.Token
conf.Token = ""
if verbose {
fmt.Println("JWT recognized")
}
} else {
conf.Token = c.Token
}
// TEMPORARY CODE - END

return conf, nil
}

Expand Down
Loading

0 comments on commit 4d81bfc

Please sign in to comment.