8000 feat(api, cli, tests): JWT access tokens management with CLI (#3987) · ovh/cds@4d81bfc · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Commit 4d81bfc

Browse files
fsaminrichardlt
authored andcommitted
feat(api, cli, tests): JWT access tokens management with CLI (#3987)
1 parent b6f202c commit 4d81bfc

Some content is hidden

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

58 files changed

+9137
-122
lines changed

cli/ask_confirm.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,30 @@ func MultiChoice(s string, opts ...string) int {
4343
return 0
4444
}
4545

46+
// MultiSelect for multiple choices question. It returns the selected options
47+
func MultiSelect(s string, opts ...string) []int {
48+
var result []string
49+
50+
if err := survey.AskOne(&survey.MultiSelect{
51+
Message: s,
52+
Options: opts,
53+
PageSize: 10,
54+
}, &result, nil); err != nil {
55+
log.Fatal(err)
56+
}
57+
58+
var choices []int
59+
for i := range opts {
60+
for j := range result {
61+
if opts[i] == result[j] {
62+
choices = append(choices, i)
63+
}
64+
}
65+
}
66+
67+
return choices
68+
}
69+
4670
// AskValueChoice ask for a string and returns it.
4771
func AskValueChoice(s string) string {
4872
var result string

cli/cdsctl/access_token.go

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
package main
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"regexp"
7+
"strconv"
8+
"strings"
9+
"time"
10+
11+
"github.com/spf13/cobra"
12+
13+
"github.com/ovh/cds/cli"
14+
"github.com/ovh/cds/sdk"
15+
)
16+
17+
func accesstoken() *cobra.Command {
18+
19+
var (
20+
cmd = cli.Command{
21+
Name: "xtoken",
22+
Short: "Manage CDS access tokens [EXPERIMENTAL]",
23+
}
24+
25+
listbyUserCmd = cli.Command{
26+
Name: "list",
27+
Short: "List your access tokens",
28+
Flags: []cli.Flag{
29+
{
30+
Name: "group",
31+
Type: cli.FlagSlice,
32+
ShortHand: "g",
33+
Usage: "filter by group",
34+
},
35+
},
36+
}
37+
38+
newCmd = cli.Command{
39+
Name: "new",
40+
Short: "Create a new access token",
41+
Flags: []cli.Flag{
42+
{
43+
Name: "description",
44+
ShortHand: "d",
45+
Usage: "what is the purpose of this token",
46+
}, {
47+
Name: "expiration",
48+
ShortHand: "e",
49+
Usage: "expiration delay of the token (1d, 24h, 1440m, 86400s)",
50+
Default: "1d",
51+
IsValid: func(s string) bool {
52+
return true
53+
},
54+
}, {
55+
Name: "group",
56+
Type: cli.FlagSlice,
57+
ShortHand: "g",
58+
Usage: "define the scope of the token through groups",
59+
},
60+
},
61+
}
62+
63+
regenCmd = cli.Command{
64+
Name: "regen",
65+
Short: "Regenerate access token",
66+
VariadicArgs: cli.Arg{
67+
Name: "token-id",
68+
AllowEmpty: false,
69+
},
70+
}
71+
72+
deleteCmd = cli.Command{
73+
Name: "delete",
74+
Short: "Delete access token",
75+
VariadicArgs: cli.Arg{
76+
Name: "token-id",
77+
AllowEmpty: true,
78+
},
79+
}
80+
)
81+
82+
return cli.NewCommand(cmd, nil,
83+
cli.SubCommands{
84+
cli.NewListCommand(listbyUserCmd, accesstokenListRun, nil),
85+
cli.NewCommand(newCmd, accesstokenNewRun, nil),
86+
cli.NewCommand(regenCmd, accesstokenRegenRun, nil),
87+
cli.NewCommand(deleteCmd, accesstokenDeleteRun, nil),
88+
},
89+
)
90+
}
91+
92+
func accesstokenListRun(v cli.Values) (cli.ListResult, error) {
93+
94+
type displayToken struct {
95+
ID string `cli:"id,key"`
96+
Description string `cli:"description"`
97+
UserName string `cli:"user"`
98+
ExpireAt string `cli:"expired_at"`
99+
Created string `cli:"created"`
100+
Status string `cli:"status"`
101+
Scope string `cli:"scope"`
102+
}
103+
104+
var displayTokenFunc = func(t sdk.AccessToken) displayToken {
105+
var groupNames []string
106+
for _, g := range t.Groups {
107+
groupNames = append(groupNames, g.Name)
108+
}
109+
return displayToken{
110+
ID: t.ID,
111+
Description: t.Description,
112+
UserName: t.User.Fullname,
113+
ExpireAt: t.ExpireAt.Format(time.RFC850),
114+
Created: t.Created.Format(time.RFC850),
115+
Status: t.Status,
116+
Scope: strings.Join(groupNames, ","),
117+
}
118+
}
119+
120+
var displayAllTokensFunc = func(ts []sdk.AccessToken) []displayToken {
121+
var res = make([]displayToken, len(ts))
122+
for i := range ts {
123+
res[i] = displayTokenFunc(ts[i])
124+
}
125+
return res
126+
}
127+
128+
groups := v.GetStringSlice("group")
129+
if len(groups) == 0 {
130+
tokens, err := client.AccessTokenListByUser(cfg.User)
131+
if err != nil {
132+
return nil, err
133+
}
134+
return cli.AsListResult(displayAllTokensFunc(tokens)), nil
135+
}
136+
137+
tokens, err := client.AccessTokenListByGroup(groups...)
138+
if err != nil {
139+
return nil, err
140+
}
141+
return cli.AsListResult(displayAllTokensFunc(tokens)), nil
142+
}
143+
144+
func accesstokenNewRun(v cli.Values) error {
145+
allGroups, err := client.GroupList()
146+
if err != nil {
147+
return err
148+
}
149+
150+
description := v.GetString("description")
151+
expiration := v.GetString("expiration")
152+
groups := v.GetStringSlice("group")
153+
154+
// If the flag has not been set, ask interactively
155+
if description == "" {
156+
description = cli.AskValueChoice("Description")
157+
}
158+
if expiration == "" {
159+
expiration = cli.AskValueChoice("Expiration")
160+
}
161+
if len(groups) == 0 {
162+
var groupNames []string
163+
for _, g := range allGroups {
164+
groupNames = append(groupNames, g.Name)
165+
}
166+
choices := cli.MultiSelect("Groups", groupNames...)
167+
for _, choice := range choices {
168+
groups = append(groups, groupNames[choice])
169+
}
170+
}
171+
172+
// Compute expiration string
173+
var r = regexp.MustCompile("([0-9])(s|m|h|d)")
174+
if !r.MatchString(expiration) {
175+
return errors.New("unsupported expiration expression")
176+
}
177+
178+
matches := r.FindStringSubmatch(expiration)
179+
factor, _ := strconv.ParseFloat(matches[1], 64)
180+
unit := time.Second
181+
switch matches[2] {
182+
case "m":
183+
unit = time.Minute
184+
case "h":
185+
unit = time.Hour
186+
case "d":
187+
unit = 24 * time.Hour
188+
}
189+
190+
expirationDuration := time.Duration(factor) * unit
191+
192+
// Retrieve group IDs from all the groups accessible by the user
193+
var groupsIDs []int64
194+
for _, group := range groups {
195+
var groupFound bool
196+
for _, knowGroup := range allGroups {
197+
if knowGroup.Name == group {
198+
groupFound = true
199+
groupsIDs = append(groupsIDs, knowGroup.ID)
200+
break
201+
}
202+
}
203+
if !groupFound {
204+
return errors.New("group not found")
205+
}
206+
}
207+
208+
var request = sdk.AccessTokenRequest{
209+
Description: description,
210+
ExpirationDelaySecond: expirationDuration.Seconds(),
211+
GroupsIDs: groupsIDs,
212+
Origin: "cdsctl",
213+
}
214+
215+
t, jwt, err := client.AccessTokenCreate(request)
216+
if err != nil {
217+
return fmt.Errorf("unable to create access token: %v", err)
218+
}
219+
fmt.Println()
220+
221+
displayToken(t, jwt)
222+
223+
return nil
224+
}
225+
226+
func displayToken(t sdk.AccessToken, jwt string) {
227+
fmt.Println("Token successfully generated")
228+
fmt.Println(cli.Cyan("ID"), "\t\t", t.ID)
229+
fmt.Println(cli.Cyan("Description"), "\t", t.Description)
230+
fmt.Println(cli.Cyan("Creation"), "\t", t.Created.Format(time.RFC850))
231+
fmt.Println(cli.Red("Expiration"), "\t", cli.Red(t.ExpireAt.Format(time.RFC850)))
232+
fmt.Println(cli.Cyan("User"), "\t\t", t.User.Fullname)
233+
var groupNames []string
234+
for _, g := range t.Groups {
235+
groupNames = append(groupNames, g.Name)
236+
}
237+
fmt.Println(cli.Cyan("Scope"), "\t\t", groupNames)
238+
fmt.Println()
239+
fmt.Println(cli.Red("Here it is, keep it in a safe place, it will never ne displayed again."))
240+
fmt.Println(jwt)
241+
}
242+
243+
func accesstokenRegenRun(v cli.Values) error {
244+
tokenIDs := v.GetStringSlice("token-id")
245+
for _, id := range tokenIDs {
246+
247+
t, jwt, err := client.AccessTokenRegen(id)
248+
if err != nil {
249+
fmt.Println("unable to regen token", id, cli.Red(err.Error()))
250+
}
251+
252+
displayToken(t, jwt)
253+
}
254+
255+
return nil
256+
}
257+
258+
func accesstokenDeleteRun(v cli.Values) error {
259+
tokenIDs := v.GetStringSlice("token-id")
260+
for _, id := range tokenIDs {
261+
if err := client.AccessTokenDelete(id); err != nil {
262+
fmt.Println("unable to delete token", id, cli.Red(err.Error()))
263+
}
264+
265+
}
266+
267+
return nil
268+
}

cli/cdsctl/config.go

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@ import (
66
"path"
77
"strconv"
88

9-
repo "github.com/fsamin/go-repo"
9+
"github.com/dgrijalva/jwt-go"
10+
"github.com/fsamin/go-repo"
1011

1112
"github.com/ovh/cds/cli"
1213
"github.com/ovh/cds/sdk"
@@ -104,11 +105,23 @@ func loadConfig(configFile string) (*cdsclient.Config, error) {
104105
conf := &cdsclient.Config{
105106
Host: c.Host,
106107
User: c.User,
107-
Token: c.Token,
108108
Verbose: verbose,
109109
InsecureSkipVerifyTLS: c.InsecureSkipVerifyTLS,
110110
}
111111

112+
// TEMPORARY CODE
113+
// Try to parse the token as JWT and set it as access token
114+
if _, _, err := new(jwt.Parser).ParseUnverified(c.Token, &sdk.AccessTokenJWTClaims{}); err == nil {
115+
conf.AccessToken = c.Token
116+
conf.Token = ""
117+
if verbose {
118+
fmt.Println("JWT recognized")
119+
}
120+
} else {
121+
conf.Token = c.Token
122+
}
123+
// TEMPORARY CODE - END
124+
112125
return conf, nil
113126
}
114127

0 commit comments

Comments
 (0)
0