Skip to content

Commit 60f1c6d

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 9bc753c commit 60f1c6d

File tree

3 files changed

+450
-2
lines changed

3 files changed

+450
-2
lines changed

cli/auth_account_command.go

+241
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424

2525
au "github.com/nats-io/natscli/internal/auth"
2626
iu "github.com/nats-io/natscli/internal/util"
27+
"gopkg.in/yaml.v3"
2728

2829
"github.com/AlecAivazis/survey/v2"
2930
"github.com/choria-io/fisk"
@@ -102,6 +103,11 @@ type authAccountCommand struct {
102103
tags []string
103104
rmTags []string
104105
signingKey string
106+
mapSource string
107+
mapTarget string
108+
mapWeight uint
109+
mapCluster string
110+
inputFile string
105111
}
106112

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

285316
func (c *authAccountCommand) selectAccount(pick bool) (*ab.AuthImpl, ab.Operator, ab.Account, error) {
@@ -1101,3 +1132,213 @@ func (c *authAccountCommand) validTiers(acct ab.Account) []int8 {
11011132

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

go.sum

-2
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=

0 commit comments

Comments
 (0)