github.com/nats-io/nsc@v0.0.0-20221206222106-35db9400b257/cmd/edituser.go (about) 1 /* 2 * Copyright 2018-2021 The NATS Authors 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16 package cmd 17 18 import ( 19 "fmt" 20 "sort" 21 "strings" 22 "time" 23 24 "github.com/nats-io/jwt/v2" 25 "github.com/nats-io/nkeys" 26 "github.com/nats-io/nsc/cmd/store" 27 "github.com/spf13/cobra" 28 ) 29 30 func createEditUserCmd() *cobra.Command { 31 var params EditUserParams 32 cmd := &cobra.Command{ 33 Use: "user", 34 Short: "Edit an user", 35 Long: `# Edit permissions so that the user can publish and/or subscribe to the specified subjects or wildcards: 36 nsc edit user --name <n> --allow-pubsub <subject>,... 37 nsc edit user --name <n> --allow-pub <subject>,... 38 nsc edit user --name <n> --allow-sub <subject>,... 39 40 # Set permissions so that the user cannot publish nor subscribe to the specified subjects or wildcards: 41 nsc edit user --name <n> --deny-pubsub <subject>,... 42 nsc edit user --name <n> --deny-pub <subject>,... 43 nsc edit user --name <n> --deny-sub <subject>,... 44 45 # Set subscribe permissions with queue names (separated from subject by space) 46 # When added this way, the corresponding remove command needs to be presented with the exact same string 47 nsc edit user --name <n> --deny-sub "<subject> <queue>,..." 48 nsc edit user --name <n> --allow-sub "<subject> <queue>,..." 49 50 # Remove a previously set permissions 51 nsc edit user --name <n> --rm <subject>,... 52 53 # To dynamically allow publishing to reply subjects, this works well for service responders: 54 nsc edit user --name <n> --allow-pub-response 55 56 # A permission to publish a response can be removed after a duration from when 57 # the message was received: 58 nsc edit user --name <n> --allow-pub-response --response-ttl 5s 59 60 # If the service publishes multiple response messages, you can specify: 61 nsc edit user --name <n> --allow-pub-response=5 62 # See 'nsc edit export --response-type --help' to enable multiple 63 # responses between accounts. 64 65 # To remove response settings: 66 nsc edit user --name <n> --rm-response-perms 67 `, 68 Args: cobra.MaximumNArgs(1), 69 SilenceUsage: true, 70 RunE: func(cmd *cobra.Command, args []string) error { 71 return RunAction(cmd, args, ¶ms) 72 }, 73 } 74 cmd.Flags().StringSliceVarP(¶ms.tags, "tag", "", nil, "add tags for user - comma separated list or option can be specified multiple times") 75 cmd.Flags().StringSliceVarP(¶ms.rmTags, "rm-tag", "", nil, "remove tag - comma separated list or option can be specified multiple times") 76 cmd.Flags().StringVarP(¶ms.name, "name", "n", "", "user name") 77 params.AccountContextParams.BindFlags(cmd) 78 params.GenericClaimsParams.BindFlags(cmd) 79 params.UserPermissionLimits.BindFlags(cmd) 80 return cmd 81 } 82 83 func init() { 84 editCmd.AddCommand(createEditUserCmd()) 85 } 86 87 type EditUserParams struct { 88 AccountContextParams 89 SignerParams 90 GenericClaimsParams 91 claim *jwt.UserClaims 92 name string 93 token string 94 credsFilePath string 95 UserPermissionLimits 96 } 97 98 func (p *EditUserParams) SetDefaults(ctx ActionCtx) error { 99 p.name = NameFlagOrArgument(p.name, ctx) 100 if err := p.AccountContextParams.SetDefaults(ctx); err != nil { 101 return err 102 } 103 p.SignerParams.SetDefaults(nkeys.PrefixByteAccount, true, ctx) 104 105 if !InteractiveFlag && ctx.NothingToDo("start", "expiry", "rm", "allow-pub", "allow-sub", "allow-pubsub", 106 "deny-pub", "deny-sub", "deny-pubsub", "tag", "rm-tag", "source-network", "rm-source-network", "payload", 107 "rm-response-perms", "max-responses", "response-ttl", "allow-pub-response", "bearer", "rm-time", "time", "conn-type", 108 "rm-conn-type", "subs", "data") { 109 ctx.CurrentCmd().SilenceUsage = false 110 return fmt.Errorf("specify an edit option") 111 } 112 return nil 113 } 114 115 func (p *EditUserParams) PreInteractive(ctx ActionCtx) error { 116 var err error 117 if err = p.AccountContextParams.Edit(ctx); err != nil { 118 return err 119 } 120 121 if p.name == "" { 122 p.name, err = ctx.StoreCtx().PickUser(p.AccountContextParams.Name) 123 if err != nil { 124 return err 125 } 126 } 127 128 signers, err := validUserSigners(ctx, p.Name) 129 if err != nil { 130 return err 131 } 132 p.SignerParams.SetPrompt("select the key to sign the user") 133 return p.SignerParams.SelectFromSigners(ctx, signers) 134 } 135 136 func (p *EditUserParams) Load(ctx ActionCtx) error { 137 var err error 138 139 if err = p.AccountContextParams.Validate(ctx); err != nil { 140 return err 141 } 142 143 if p.name == "" { 144 n := ctx.StoreCtx().DefaultUser(p.AccountContextParams.Name) 145 if n != nil { 146 p.name = *n 147 } 148 } 149 150 if p.name == "" { 151 ctx.CurrentCmd().SilenceUsage = false 152 return fmt.Errorf("user name is required") 153 } 154 155 if !ctx.StoreCtx().Store.Has(store.Accounts, p.AccountContextParams.Name, store.Users, store.JwtName(p.name)) { 156 return fmt.Errorf("user %q not found", p.name) 157 } 158 159 p.claim, err = ctx.StoreCtx().Store.ReadUserClaim(p.AccountContextParams.Name, p.name) 160 if err != nil { 161 return err 162 } 163 164 p.UserPermissionLimits.Load(ctx, p.claim.UserPermissionLimits) 165 166 return err 167 } 168 169 func (p *EditUserParams) PostInteractive(ctx ActionCtx) error { 170 // FIXME: we won't do interactive on the response params until pub/sub/deny permissions are interactive 171 //if err := p.PermissionsParams.Edit(p.claim.Resp != nil); err != nil { 172 // return err 173 //} 174 if err := p.UserPermissionLimits.PostInteractive(ctx); err != nil { 175 return err 176 } 177 if p.claim.NotBefore > 0 { 178 p.GenericClaimsParams.Start = UnixToDate(p.claim.NotBefore) 179 } 180 if p.claim.Expires > 0 { 181 p.GenericClaimsParams.Expiry = UnixToDate(p.claim.Expires) 182 } 183 if err := p.GenericClaimsParams.Edit(p.claim.Tags); err != nil { 184 return err 185 } 186 return nil 187 } 188 189 func (p *EditUserParams) Validate(ctx ActionCtx) error { 190 p.UserPermissionLimits.Validate(ctx) 191 192 if err := p.GenericClaimsParams.Valid(); err != nil { 193 return err 194 } 195 if err := p.SignerParams.ResolveWithPriority(ctx, p.claim.Issuer); err != nil { 196 return err 197 } 198 199 if ac, err := ctx.StoreCtx().Store.ReadAccountClaim(p.AccountContextParams.Name); err != nil { 200 return err 201 } else if ac.Limits.DisallowBearer && p.bearer { 202 return fmt.Errorf("account disallows bearer token") 203 } 204 return nil 205 } 206 207 func (p *EditUserParams) Run(ctx ActionCtx) (store.Status, error) { 208 r := store.NewDetailedReport(true) 209 r.ReportSum = false 210 211 var err error 212 if err := p.GenericClaimsParams.Run(ctx, p.claim, r); err != nil { 213 return nil, err 214 } 215 216 s, err := p.UserPermissionLimits.Run(ctx, &p.claim.UserPermissionLimits) 217 if err != nil { 218 return nil, err 219 } 220 if s != nil { 221 r.Add(s.Details...) 222 } 223 224 // get the account JWT - must have since we resolved the user based on it 225 ac, err := ctx.StoreCtx().Store.ReadAccountClaim(p.AccountContextParams.Name) 226 if err != nil { 227 return nil, err 228 } 229 230 // extract the signer public key 231 pk, err := p.signerKP.PublicKey() 232 if err != nil { 233 return nil, err 234 } 235 // signer doesn't match - so we set IssuerAccount to the account 236 if pk != ac.Subject { 237 p.claim.IssuerAccount = ac.Subject 238 } 239 240 if err := checkUserForScope(ctx, p.AccountContextParams.Name, p.signerKP, p.claim); err != nil { 241 r.AddFromError(err) 242 r.AddWarning("user was NOT edited as the edits conflict with signing key scope") 243 return r, err 244 } 245 246 // we sign 247 p.token, err = p.claim.Encode(p.signerKP) 248 if err != nil { 249 return nil, err 250 } 251 252 // if the signer is not allowed, the store will reject 253 rs, err := ctx.StoreCtx().Store.StoreClaim([]byte(p.token)) 254 if rs != nil { 255 r.Add(rs) 256 } 257 if err != nil { 258 r.AddFromError(err) 259 } 260 if rs != nil { 261 r.Add(rs) 262 } 263 ks := ctx.StoreCtx().KeyStore 264 if ks.HasPrivateKey(p.claim.Subject) { 265 ukp, err := ks.GetKeyPair(p.claim.Subject) 266 if err != nil { 267 r.AddError("unable to read keypair: %v", err) 268 } 269 d, err := GenerateConfig(ctx.StoreCtx().Store, p.AccountContextParams.Name, p.name, ukp) 270 if err != nil { 271 r.AddError("unable to save creds: %v", err) 272 } else { 273 p.credsFilePath, err = ks.MaybeStoreUserCreds(p.AccountContextParams.Name, p.name, d) 274 if err != nil { 275 r.AddError("error storing creds: %v", err) 276 } else { 277 r.AddOK("generated user creds file %#q", AbbrevHomePaths(p.credsFilePath)) 278 } 279 } 280 } else { 281 r.AddOK("skipped generating creds file - user private key is not available") 282 } 283 if r.HasNoErrors() { 284 r.AddOK("edited user %q", p.name) 285 } 286 return r, nil 287 } 288 289 const timeFormat = "hh:mm:ss" 290 const timeLayout = "15:04:05" 291 292 type timeSlice []jwt.TimeRange 293 294 func (t *timeSlice) Set(val string) error { 295 if tk := strings.Split(val, "-"); len(tk) != 2 { 296 return fmt.Errorf(`require format: "%s-%s" got "%s"`, timeLayout, timeLayout, val) 297 } else if _, err := time.Parse(timeLayout, tk[0]); err != nil { 298 return fmt.Errorf(`require format: "%s-%s" could not parse start time "%s"`, timeLayout, timeLayout, tk[0]) 299 } else if _, err := time.Parse(timeLayout, tk[1]); err != nil { 300 return fmt.Errorf(`require format: "%s-%s" could not parse end time "%s"`, timeLayout, timeLayout, tk[1]) 301 } else { 302 *t = append(*t, jwt.TimeRange{Start: tk[0], End: tk[1]}) 303 return nil 304 } 305 } 306 307 func (t *timeSlice) String() string { 308 values := make([]string, len(*t)) 309 for i, r := range *t { 310 values[i] = fmt.Sprintf("%s-%s", r.Start, r.End) 311 } 312 return "[" + strings.Join(values, ",") + "]" 313 } 314 315 func (t *timeSlice) Type() string { 316 return "time-ranges" 317 } 318 319 type timeLocale string 320 321 func (l *timeLocale) Set(val string) error { 322 v, err := time.LoadLocation(val) 323 if err == nil { 324 *l = timeLocale(v.String()) 325 } 326 return err 327 } 328 329 func (l *timeLocale) String() string { 330 return string(*l) 331 } 332 333 func (t *timeLocale) Type() string { 334 return "time-locale" 335 } 336 337 type UserPermissionLimits struct { 338 PermissionsParams 339 bearer bool 340 payload NumberParams 341 maxData NumberParams 342 maxSubs int64 343 rmConnTypes []string 344 connTypes []string 345 rmSrc []string 346 src []string 347 locale timeLocale 348 rmTimes []string 349 times timeSlice 350 } 351 352 func (p *UserPermissionLimits) BindFlags(cmd *cobra.Command) { 353 cmd.Flags().VarP(&p.times, "time", "", fmt.Sprintf(`add start-end time range of the form "%s-%s" (option can be specified multiple times)`, timeFormat, timeFormat)) 354 cmd.Flags().StringSliceVarP(&p.rmTimes, "rm-time", "", nil, fmt.Sprintf(`remove start-end time by start time "%s" (option can be specified multiple times)`, timeFormat)) 355 cmd.Flags().VarP(&p.locale, "locale", "", "set the locale with which time values are interpreted") 356 cmd.Flags().StringSliceVarP(&p.src, "source-network", "", nil, "add source network for connection - comma separated list or option can be specified multiple times") 357 cmd.Flags().StringSliceVarP(&p.rmSrc, "rm-source-network", "", nil, "remove source network for connection - comma separated list or option can be specified multiple times") 358 cmd.Flags().StringSliceVarP(&p.connTypes, "conn-type", "", nil, fmt.Sprintf("set allowed connection types: %s %s %s %s %s %s - comma separated list or option can be specified multiple times", 359 jwt.ConnectionTypeLeafnode, jwt.ConnectionTypeMqtt, jwt.ConnectionTypeStandard, jwt.ConnectionTypeWebsocket, jwt.ConnectionTypeLeafnodeWS, jwt.ConnectionTypeMqttWS)) 360 cmd.Flags().StringSliceVarP(&p.rmConnTypes, "rm-conn-type", "", nil, "remove connection types - comma separated list or option can be specified multiple times") 361 cmd.Flags().Int64VarP(&p.maxSubs, "subs", "", -1, "set maximum number of subscriptions (-1 is unlimited)") 362 p.maxData = -1 363 cmd.Flags().VarP(&p.maxData, "data", "", "set maximum data in bytes for the user (-1 is unlimited)") 364 p.payload = -1 365 cmd.Flags().VarP(&p.payload, "payload", "", "set maximum message payload in bytes for the account (-1 is unlimited)") 366 cmd.Flags().BoolVarP(&p.bearer, "bearer", "", false, "no connect challenge required for user") 367 p.PermissionsParams.bindSetFlags(cmd, "permissions") 368 p.PermissionsParams.bindRemoveFlags(cmd, "permissions") 369 } 370 371 func (p *UserPermissionLimits) Load(ctx ActionCtx, u jwt.UserPermissionLimits) error { 372 if !ctx.CurrentCmd().Flag("payload").Changed { 373 p.payload = NumberParams(u.Limits.Payload) 374 } 375 if !ctx.CurrentCmd().Flag("data").Changed { 376 p.maxData = NumberParams(u.Limits.Data) 377 } 378 if !ctx.CurrentCmd().Flag("subs").Changed { 379 p.maxSubs = u.Limits.Subs 380 } 381 return nil 382 } 383 384 func (p *UserPermissionLimits) PostInteractive(_ ActionCtx) error { 385 // FIXME: we won't do interactive on the response params until pub/sub/deny permissions are interactive 386 //if err := p.PermissionsParams.Edit(p.claim.Resp != nil); err != nil { 387 // return err 388 //} 389 if err := p.payload.Edit("max payload (-1 unlimited)"); err != nil { 390 return err 391 } 392 return nil 393 } 394 395 func (p *UserPermissionLimits) Validate(ctx ActionCtx) error { 396 connTypes := make([]string, len(p.connTypes)) 397 for i, k := range p.connTypes { 398 u := strings.ToUpper(k) 399 switch u { 400 case jwt.ConnectionTypeLeafnode, jwt.ConnectionTypeMqtt, jwt.ConnectionTypeStandard, 401 jwt.ConnectionTypeWebsocket, jwt.ConnectionTypeLeafnodeWS, jwt.ConnectionTypeMqttWS: 402 default: 403 return fmt.Errorf("unknown connection type %s", k) 404 } 405 connTypes[i] = u 406 } 407 rmConnTypes := make([]string, len(p.rmConnTypes)) 408 for i, k := range p.rmConnTypes { 409 rmConnTypes[i] = strings.ToUpper(k) 410 } 411 p.rmConnTypes = rmConnTypes 412 413 if err := p.PermissionsParams.Validate(); err != nil { 414 return err 415 } 416 417 return nil 418 } 419 420 func (p *UserPermissionLimits) Run(ctx ActionCtx, claim *jwt.UserPermissionLimits) (*store.Report, error) { 421 r := store.NewDetailedReport(true) 422 r.ReportSum = false 423 424 var err error 425 426 flags := ctx.CurrentCmd().Flags() 427 claim.Limits.Payload = p.payload.Int64() 428 if flags.Changed("payload") { 429 r.AddOK("changed max imports to %d", claim.Limits.Payload) 430 } 431 claim.Limits.Data = p.maxData.Int64() 432 if flags.Changed("data") { 433 r.AddOK("changed max data to %d", claim.Limits.Data) 434 } 435 claim.Limits.Subs = p.maxSubs 436 if flags.Changed("subs") { 437 r.AddOK("changed max number of subs to %d", claim.Limits.Subs) 438 } 439 440 if flags.Changed("bearer") { 441 claim.BearerToken = p.bearer 442 if flags.Lookup("bearer").DefValue != fmt.Sprint(p.bearer) { 443 r.AddOK("changed bearer to %t", p.bearer) 444 } else { 445 r.AddOK("ignoring change to bearer - value is already %t", p.bearer) 446 } 447 } 448 449 var connTypes jwt.StringList 450 connTypes.Add(claim.AllowedConnectionTypes...) 451 connTypes.Add(p.connTypes...) 452 for _, v := range p.connTypes { 453 r.AddOK("added connection type %s", v) 454 } 455 connTypes.Remove(p.rmConnTypes...) 456 for _, v := range p.rmConnTypes { 457 r.AddOK("removed connection type %s", v) 458 } 459 claim.AllowedConnectionTypes = connTypes 460 461 var srcList jwt.CIDRList 462 srcList.Add(claim.Src...) 463 srcList.Add(p.src...) 464 for _, v := range p.src { 465 r.AddOK("added src network %s", v) 466 } 467 srcList.Remove(p.rmSrc...) 468 for _, v := range p.rmSrc { 469 r.AddOK("removed src network %s", v) 470 } 471 sort.Strings(srcList) 472 claim.Src = srcList 473 474 if flags.Changed("locale") { 475 claim.Locale = p.locale.String() 476 } 477 for _, v := range p.times { 478 r.AddOK("added time range %s-%s", v.Start, v.End) 479 claim.Times = append(claim.Times, v) 480 } 481 for _, vDel := range p.rmTimes { 482 for i, v := range claim.Times { 483 if v.Start == vDel { 484 r.AddOK("removed time range %s-%s", v.Start, v.End) 485 a := claim.Times 486 // Remove the element at index i from a. 487 copy(a[i:], a[i+1:]) // Shift a[i+1:] left one index. 488 a[len(a)-1] = jwt.TimeRange{} // Erase last element (write zero value). 489 claim.Times = a[:len(a)-1] // Truncate slice. 490 break 491 } 492 } 493 } 494 495 s, err := p.PermissionsParams.Run(&claim.Permissions, ctx) 496 if err != nil { 497 return nil, err 498 } 499 if s != nil { 500 r.Add(s.Details...) 501 } 502 503 return r, nil 504 }