Skip to content

Commit f6cd1e4

Browse files
committed
(nats-io#1268) Add mappings action to auth command
mappings has `add`, `rm`, `list` and `info` commands. Add can take a --config flag pointing at a valid jwt, which it will then parse and extract the mappings from it.
1 parent 9bc753c commit f6cd1e4

File tree

4 files changed

+395
-13
lines changed

4 files changed

+395
-13
lines changed

cli/auth_account_command.go

+246
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"strconv"
2323
"time"
2424

25+
"github.com/nats-io/jwt/v2"
2526
au "github.com/nats-io/natscli/internal/auth"
2627
iu "github.com/nats-io/natscli/internal/util"
2728

@@ -102,6 +103,10 @@ type authAccountCommand struct {
102103
tags []string
103104
rmTags []string
104105
signingKey string
106+
mapSource string
107+
mapTarget string
108+
mapWeight uint
109+
inputFile string
105110
}
106111

107112
func configureAuthAccountCommand(auth commandHost) {
@@ -280,6 +285,30 @@ func configureAuthAccountCommand(auth commandHost) {
280285
skrm.Flag("key", "The key to remove").StringVar(&c.skRole)
281286
skrm.Flag("operator", "Operator to act on").StringVar(&c.operatorName)
282287
skrm.Flag("force", "Removes without prompting").Short('f').UnNegatableBoolVar(&c.force)
288+
289+
mappings := acct.Command("mappings", "Manage account level subject mapping and partitioning").Alias("m")
290+
291+
mappingsaAdd := mappings.Command("add", "Add a new mapping").Alias("new").Alias("a").Action(c.mappingAddAction)
292+
mappingsaAdd.Arg("account", "Account to create the mappings on").StringVar(&c.accountName)
293+
mappingsaAdd.Arg("source", "The source subject of the mapping").StringVar(&c.mapSource)
294+
mappingsaAdd.Arg("target", "The target subject of the mapping").StringVar(&c.mapTarget)
295+
mappingsaAdd.Arg("weight", "The weight (%) of the mappingmapping").Default("100").UintVar(&c.mapWeight)
296+
mappingsaAdd.Flag("operator", "Operator to act on").StringVar(&c.operatorName)
297+
mappingsaAdd.Flag("config", "JWT file to read configuration from").ExistingFileVar(&c.inputFile)
298+
299+
mappingsls := mappings.Command("ls", "List mappings").Alias("list").Action(c.mappingListAction)
300+
mappingsls.Arg("account", "Account to create the mappings on").StringVar(&c.accountName)
301+
mappingsls.Flag("operator", "Operator to act on").StringVar(&c.operatorName)
302+
303+
mappingsrm := mappings.Command("rm", "Remove a mapping").Action(c.mappingRmAction)
304+
mappingsrm.Arg("account", "Account to create the mappings on").StringVar(&c.accountName)
305+
mappingsrm.Arg("source", "The source subject of the mapping").StringVar(&c.mapSource)
306+
mappingsrm.Flag("operator", "Operator to act on").StringVar(&c.operatorName)
307+
308+
mappingsinfo := mappings.Command("info", "Show information about a mapping").Alias("i").Alias("show").Alias("view").Action(c.mappingInfoAction)
309+
mappingsinfo.Arg("account", "Account to create the mappings on").StringVar(&c.accountName)
310+
mappingsinfo.Arg("source", "The source subject of the mapping").StringVar(&c.mapSource)
311+
mappingsinfo.Flag("operator", "Operator to act on").StringVar(&c.operatorName)
283312
}
284313

285314
func (c *authAccountCommand) selectAccount(pick bool) (*ab.AuthImpl, ab.Operator, ab.Account, error) {
@@ -1101,3 +1130,220 @@ func (c *authAccountCommand) validTiers(acct ab.Account) []int8 {
11011130

11021131
return tiers
11031132
}
1133+
1134+
func (c *authAccountCommand) loadJwt() (*jwt.AccountClaims, error) {
1135+
if c.inputFile != "" {
1136+
f, err := os.ReadFile(c.inputFile)
1137+
if err != nil {
1138+
return nil, err
1139+
}
1140+
1141+
claims, err := jwt.DecodeAccountClaims(string(f))
1142+
if err != nil {
1143+
return nil, fmt.Errorf("failed to decode JWT: %w", err)
1144+
}
1145+
return claims, nil
1146+
}
1147+
return nil, nil
1148+
1149+
}
1150+
1151+
func (c *authAccountCommand) parseJwtMappings(mappings map[string][]ab.Mapping, jwtMappings jwt.Mapping) {
1152+
for subject, weightedMappings := range jwtMappings {
1153+
mappings[string(subject)] = []ab.Mapping{}
1154+
for _, m := range weightedMappings {
1155+
mappings[string(subject)] = append(mappings[string(subject)], ab.Mapping{Weight: m.Weight, Subject: string(m.Subject), Cluster: m.Cluster})
1156+
}
1157+
}
1158+
}
1159+
1160+
func (c *authAccountCommand) mappingAddAction(_ *fisk.ParseContext) error {
1161+
mappings := map[string][]ab.Mapping{}
1162+
if c.inputFile != "" {
1163+
cfg, err := c.loadJwt()
1164+
if err != nil {
1165+
return err
1166+
}
1167+
c.accountName = cfg.Name
1168+
c.parseJwtMappings(mappings, cfg.Mappings)
1169+
}
1170+
1171+
auth, _, acct, err := c.selectAccount(true)
1172+
if err != nil {
1173+
return err
1174+
}
1175+
1176+
if c.inputFile == "" {
1177+
if c.mapSource == "" {
1178+
err := iu.AskOne(&survey.Input{
1179+
Message: "Source subject",
1180+
Help: "The source subject of the mapping",
1181+
}, &c.mapSource, survey.WithValidator(survey.Required))
1182+
if err != nil {
1183+
return err
1184+
}
1185+
}
1186+
1187+
if c.mapTarget == "" {
1188+
err := iu.AskOne(&survey.Input{
1189+
Message: "Target subject",
1190+
Help: "The target subject of the mapping",
1191+
}, &c.mapTarget, survey.WithValidator(survey.Required))
1192+
if err != nil {
1193+
return err
1194+
}
1195+
}
1196+
1197+
mapping := ab.Mapping{Subject: c.mapTarget, Weight: uint8(c.mapWeight)}
1198+
// check if there are mappings already set for the source
1199+
currentMappings := acct.SubjectMappings().Get(c.mapSource)
1200+
if len(currentMappings) > 0 {
1201+
// Check that we don't overwrite the current mapping
1202+
for _, m := range currentMappings {
1203+
if m.Subject == c.mapTarget {
1204+
return fmt.Errorf("mapping %s -> %s already exists", c.mapSource, c.mapTarget)
1205+
}
1206+
}
1207+
}
1208+
currentMappings = append(currentMappings, mapping)
1209+
mappings[c.mapSource] = currentMappings
1210+
}
1211+
1212+
for subject, m := range mappings {
1213+
err = acct.SubjectMappings().Set(subject, m...)
1214+
if err != nil {
1215+
return err
1216+
}
1217+
}
1218+
1219+
err = auth.Commit()
1220+
if err != nil {
1221+
return err
1222+
}
1223+
1224+
return c.fShowMappings(os.Stdout, mappings)
1225+
}
1226+
1227+
func (c *authAccountCommand) mappingInfoAction(_ *fisk.ParseContext) error {
1228+
_, _, acct, err := c.selectAccount(true)
1229+
if err != nil {
1230+
return err
1231+
}
1232+
1233+
accountMappings := acct.SubjectMappings().List()
1234+
if len(accountMappings) == 0 {
1235+
fmt.Println("No mappings defined")
1236+
return nil
1237+
}
1238+
1239+
if c.mapSource == "" {
1240+
err = iu.AskOne(&survey.Select{
1241+
Message: "Select a mapping to inspect",
1242+
Options: accountMappings,
1243+
PageSize: iu.SelectPageSize(len(accountMappings)),
1244+
}, &c.mapSource)
1245+
if err != nil {
1246+
return err
1247+
}
1248+
}
1249+
1250+
mappings := map[string][]ab.Mapping{
1251+
c.mapSource: acct.SubjectMappings().Get(c.mapSource),
1252+
}
1253+
1254+
return c.fShowMappings(os.Stdout, mappings)
1255+
}
1256+
1257+
func (c *authAccountCommand) mappingListAction(_ *fisk.ParseContext) error {
1258+
_, _, acct, err := c.selectAccount(true)
1259+
if err != nil {
1260+
return err
1261+
}
1262+
1263+
mappings := acct.SubjectMappings().List()
1264+
if len(mappings) == 0 {
1265+
fmt.Println("No mappings defined")
1266+
return nil
1267+
}
1268+
1269+
tbl := iu.NewTableWriter(opts(), "Subject mappings for account %s", acct.Name())
1270+
tbl.AddHeaders("Source Subject", "Target Subject", "Weight", "Cluster")
1271+
1272+
for _, fromMapping := range acct.SubjectMappings().List() {
1273+
subjectMaps := acct.SubjectMappings().Get(fromMapping)
1274+
for _, m := range subjectMaps {
1275+
tbl.AddRow(fromMapping, m.Subject, m.Weight, m.Cluster)
1276+
}
1277+
}
1278+
1279+
fmt.Println(tbl.Render())
1280+
return nil
1281+
}
1282+
1283+
func (c *authAccountCommand) mappingRmAction(_ *fisk.ParseContext) error {
1284+
auth, _, acct, err := c.selectAccount(true)
1285+
if err != nil {
1286+
return err
1287+
}
1288+
1289+
mappings := acct.SubjectMappings().List()
1290+
if len(mappings) == 0 {
1291+
fmt.Println("No mappings defined")
1292+
return nil
1293+
}
1294+
1295+
if c.mapSource == "" {
1296+
err = iu.AskOne(&survey.Select{
1297+
Message: "Select a mapping to delete",
1298+
Options: mappings,
1299+
PageSize: iu.SelectPageSize(len(mappings)),
1300+
}, &c.mapSource)
1301+
if err != nil {
1302+
return err
1303+
}
1304+
}
1305+
1306+
err = acct.SubjectMappings().Delete(c.mapSource)
1307+
if err != nil {
1308+
return err
1309+
}
1310+
1311+
err = auth.Commit()
1312+
if err != nil {
1313+
return err
1314+
}
1315+
1316+
fmt.Printf("Deleted mapping {%s}\n", c.mapSource)
1317+
return nil
1318+
}
1319+
1320+
func (c *authAccountCommand) fShowMappings(w io.Writer, mappings map[string][]ab.Mapping) error {
1321+
out, err := c.showMappings(mappings)
1322+
if err != nil {
1323+
return err
1324+
}
1325+
1326+
_, err = fmt.Fprintln(w, out)
1327+
return err
1328+
}
1329+
1330+
func (c *authAccountCommand) showMappings(mappings map[string][]ab.Mapping) (string, error) {
1331+
cols := newColumns("Subject mappings")
1332+
cols.AddSectionTitle("Configuration")
1333+
for source, m := range mappings {
1334+
// Remove when delete is fixed
1335+
totalWeight := 0
1336+
for _, wm := range m {
1337+
cols.AddRow("Source", source)
1338+
cols.AddRow("Target", wm.Subject)
1339+
cols.AddRow("Weight", wm.Weight)
1340+
cols.AddRow("Cluster", wm.Cluster)
1341+
cols.AddRow("", "")
1342+
totalWeight += int(wm.Weight)
1343+
}
1344+
cols.AddRow("Total weight:", totalWeight)
1345+
cols.AddRow("", "")
1346+
}
1347+
1348+
return cols.Render()
1349+
}

go.mod

+3-3
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,11 @@ require (
6161
github.com/rivo/uniseg v0.4.7 // indirect
6262
github.com/shopspring/decimal v1.4.0 // indirect
6363
github.com/spf13/cast v1.7.1 // indirect
64-
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
65-
golang.org/x/net v0.37.0 // indirect
64+
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect
65+
golang.org/x/net v0.35.0 // indirect
6666
golang.org/x/sys v0.31.0 // indirect
6767
golang.org/x/text v0.23.0 // indirect
68-
golang.org/x/time v0.11.0 // indirect
68+
golang.org/x/time v0.10.0 // indirect
6969
google.golang.org/protobuf v1.36.5 // indirect
7070
gopkg.in/yaml.v2 v2.4.0 // indirect
7171
)

go.sum

+8-10
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,6 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
152152
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
153153
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
154154
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
155-
github.com/synadia-io/jwt-auth-builder.go v0.0.6 h1:F3bTGWlKzWHwRqtTt35fRmhrxXLgkI8qz8QvvzxKSko=
156-
github.com/synadia-io/jwt-auth-builder.go v0.0.6/go.mod h1:8WYR7+nLQcDMBpocuPgdFJ5/2UOr+HPll3qv+KNdGvs=
157155
github.com/synadia-io/jwt-auth-builder.go v0.0.7-0.20250307212657-0e3f1ee00864 h1:itO+DjIffRn+nN3jHxHNcCiJIsL1BMZF7p3wYeTN7xs=
158156
github.com/synadia-io/jwt-auth-builder.go v0.0.7-0.20250307212657-0e3f1ee00864/go.mod h1:8WYR7+nLQcDMBpocuPgdFJ5/2UOr+HPll3qv+KNdGvs=
159157
github.com/tylertreat/hdrhistogram-writer v0.0.0-20210816161836-2e440612a39f h1:SGznmvCovewbaSgBsHgdThtWsLj5aCLX/3ZXMLd1UD0=
@@ -169,8 +167,8 @@ golang.org/x/exp v0.0.0-20180807140117-3d87b88a115f/go.mod h1:CJ0aWSM057203Lf6IL
169167
golang.org/x/exp v0.0.0-20190125153040-c74c464bbbf2/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
170168
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
171169
golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY=
172-
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
173-
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
170+
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa h1:t2QcU6V556bFjYgu4L6C+6VrCPyJZ+eyRsABUPs1mz4=
171+
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
174172
golang.org/x/image v0.0.0-20180708004352-c73c2afc3b81/go.mod h1:ux5Hcp/YLpHSI86hEcLt0YII63i6oz57MZXIpbrjZUs=
175173
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
176174
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@@ -181,8 +179,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn
181179
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
182180
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
183181
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
184-
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
185-
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
182+
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
183+
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
186184
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
187185
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
188186
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@@ -207,16 +205,16 @@ golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
207205
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
208206
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
209207
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
210-
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
211-
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
208+
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
209+
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
212210
golang.org/x/tools v0.0.0-20180525024113-a5b4c53f6e8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
213211
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
214212
golang.org/x/tools v0.0.0-20190206041539-40960b6deb8e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
215213
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
216214
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
217215
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
218-
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
219-
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
216+
golang.org/x/tools v0.30.0 h1:BgcpHewrV5AUp2G9MebG4XPFI1E2W41zU1SaqVA9vJY=
217+
golang.org/x/tools v0.30.0/go.mod h1:c347cR/OJfw5TI+GfX7RUPNMdDRRbjvYTS0jPyvsVtY=
220218
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
221219
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
222220
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

0 commit comments

Comments
 (0)