Skip to content

Commit 1c531ba

Browse files
committed
(#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 519fb95 commit 1c531ba

File tree

3 files changed

+260
-6
lines changed

3 files changed

+260
-6
lines changed

cli/auth_account_command.go

+257
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

@@ -101,6 +102,10 @@ type authAccountCommand struct {
101102
prefix string
102103
tags []string
103104
rmTags []string
105+
mapSource string
106+
mapTarget string
107+
mapWeight uint
108+
inputFile string
104109
}
105110

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

283312
func (c *authAccountCommand) selectAccount(pick bool) (*ab.AuthImpl, ab.Operator, ab.Account, error) {
@@ -1084,3 +1113,231 @@ func (c *authAccountCommand) validTiers(acct ab.Account) []int8 {
10841113

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

go.mod

+1-2
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ require (
2626
github.com/nats-io/nuid v1.0.1
2727
github.com/prometheus/client_golang v1.21.0
2828
github.com/santhosh-tekuri/jsonschema/v5 v5.3.1
29-
github.com/synadia-io/jwt-auth-builder.go v0.0.4
29+
github.com/synadia-io/jwt-auth-builder.go v0.0.6
3030
github.com/tylertreat/hdrhistogram-writer v0.0.0-20210816161836-2e440612a39f
3131
golang.org/x/crypto v0.33.0
3232
golang.org/x/term v0.29.0
@@ -61,7 +61,6 @@ 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-
go.uber.org/automaxprocs v1.6.0 // indirect
6564
golang.org/x/exp v0.0.0-20250218142911-aa4b98e5adaa // indirect
6665
golang.org/x/net v0.35.0 // indirect
6766
golang.org/x/sys v0.30.0 // indirect

go.sum

+2-4
Original file line numberDiff line numberDiff line change
@@ -152,13 +152,11 @@ 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.4 h1:cfTMDAa9iylnD/O6kXqE8Mk51F36kyuQ6BhRrT1svfI=
156-
github.com/synadia-io/jwt-auth-builder.go v0.0.4/go.mod h1:8WYR7+nLQcDMBpocuPgdFJ5/2UOr+HPll3qv+KNdGvs=
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=
157157
github.com/tylertreat/hdrhistogram-writer v0.0.0-20210816161836-2e440612a39f h1:SGznmvCovewbaSgBsHgdThtWsLj5aCLX/3ZXMLd1UD0=
158158
github.com/tylertreat/hdrhistogram-writer v0.0.0-20210816161836-2e440612a39f/go.mod h1:IY84XkhrEJTdHYLNy/zObs8mXuUAp9I65VyarbPSCCY=
159159
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
160-
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
161-
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
162160
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
163161
golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
164162
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=

0 commit comments

Comments
 (0)