github.com/tommi2day/pwcli@v0.0.0-20240317203041-4d1177a5ab91/cmd/ldap.go (about) 1 package cmd 2 3 import ( 4 "fmt" 5 "os" 6 "strings" 7 8 ldap "github.com/go-ldap/ldap/v3" 9 10 "github.com/manifoldco/promptui" 11 log "github.com/sirupsen/logrus" 12 "github.com/spf13/cobra" 13 "github.com/spf13/viper" 14 "github.com/tommi2day/gomodules/common" 15 "github.com/tommi2day/gomodules/ldaplib" 16 "github.com/tommi2day/gomodules/pwlib" 17 ) 18 19 var ldapServer = "" 20 var ldapBindDN = "" 21 var ldapBindPassword = "" 22 var ldapBaseDN = "" 23 var targetDN = "" 24 var ldapPort = 0 25 var ldapInsecure = false 26 var ldapTLS = false 27 var ldapTimeout = 20 28 var ldapGroupBase = "" 29 var ldapTargetUser = "" 30 var inputReader = os.Stdin 31 32 const ldapPublicKeyObjectClass = "ldapPublicKey" 33 const ldapSSHAttr = "sshPublicKey" 34 35 // nolint gosec 36 const ldapPasswordProfile = "8 1 1 1 0 0" 37 38 // var ldapUserContext = "ou=users" 39 40 var ldapCmd = &cobra.Command{ 41 Use: "ldap", 42 Short: "commands related to ldap", 43 } 44 45 // ldapPassCmd represents the new command 46 var ldapPassCmd = &cobra.Command{ 47 Use: "setpass", 48 Aliases: []string{"change-password"}, 49 Short: "change LDAP Password for given User per DN", 50 Long: `set new ldap password by --new-password or Env LDAP_NEW_PASSWORD for the actual bind DN or as admin bind for a target DN. 51 if no new password given some systems will generate a password`, 52 RunE: setLdapPass, 53 SilenceUsage: true, 54 } 55 56 // ldapPassCmd represents the new command 57 var ldapSSHCmd = &cobra.Command{ 58 Use: "setssh", 59 Aliases: []string{"change-sshpubkey"}, 60 Short: "Set public SSH Key to LDAP DN", 61 Long: `set new ssh public key(attribute sshPublicKey) for a given User per DN, the key must be in a file given by --sshpubkeyfile or default id_rsa.pub.`, 62 RunE: setSSHKey, 63 SilenceUsage: true, 64 } 65 66 // ldapShowCmd represents the new command 67 var ldapShowCmd = &cobra.Command{ 68 Use: "show", 69 Aliases: []string{"show-attributes", "attributes"}, 70 Short: "Show attributes of LDAP DN", 71 Long: `This command shows the attributes off the own User(Bind User) or 72 you may lookup a User cn and show the attributes of the first entry returned.`, 73 RunE: showAttributes, 74 SilenceUsage: true, 75 } 76 77 // ldapGroupCmd represents the new command 78 var ldapGroupCmd = &cobra.Command{ 79 Use: "groups", 80 Aliases: []string{"show-groups", "group-membership"}, 81 Short: "Show the group memberships of the given DN", 82 Long: `This command shows the group membership of own User(Bind User) or 83 you may lookup a User cn and if found show the groups of the first entry returned`, 84 RunE: showGroups, 85 SilenceUsage: true, 86 } 87 var hideFlags = func(command *cobra.Command, strings []string) { 88 // Hide flag for this command 89 _ = command.Flags().MarkHidden("app") 90 _ = command.Flags().MarkHidden("keydir") 91 _ = command.Flags().MarkHidden("datadir") 92 _ = command.Flags().MarkHidden("config") 93 _ = command.Flags().MarkHidden("method") 94 // Call parent help func 95 command.Parent().HelpFunc()(command, strings) 96 } 97 98 func showGroups(_ *cobra.Command, _ []string) error { 99 log.Debugf("ldap groups called") 100 lc, err := ldapLogin() 101 if err != nil { 102 log.Warnf("ldap login returned error %v", err) 103 return err 104 } 105 106 // validate parameter 107 if ldapGroupBase == "" { 108 ldapGroupBase = ldapBaseDN 109 } 110 111 // lookup target user if given 112 udn := "" 113 if ldapTargetUser != "" { 114 udn, err = lookupTargetUser(lc, ldapTargetUser) 115 if err != nil { 116 log.Errorf("%v", err) 117 return err 118 } 119 if udn != "" { 120 targetDN = udn 121 } 122 } 123 log.Debugf("targetDN:%s", targetDN) 124 // search for targetDN entry 125 filter := fmt.Sprintf("(|(&(objectclass=groupOfUniqueNames)(uniqueMember=%s))(&(objectclass=groupOfNames)(member=%s)))", targetDN, targetDN) 126 log.Debugf("ldap search for groups with filter %s", filter) 127 entries, err := lc.Search(ldapGroupBase, filter, []string{"DN"}, ldap.ScopeWholeSubtree, ldap.DerefInSearching) 128 if err != nil { 129 log.Errorf("search for %s returned error %v", targetDN, err) 130 return fmt.Errorf("search for %s returned error %v", targetDN, err) 131 } 132 if len(entries) == 0 { 133 log.Warnf("no groups for %s found", targetDN) 134 fmt.Printf("no groups for %s found", targetDN) 135 return nil 136 } 137 fmt.Printf("DN '%s' is member of the following groups:\n", targetDN) 138 for _, e := range entries { 139 log.Infof("Group: %s", e.DN) 140 fmt.Printf("Group: %s\n", e.DN) 141 } 142 return nil 143 } 144 145 func showAttributes(cmd *cobra.Command, _ []string) error { 146 log.Debugf("ldap show called") 147 lc, err := ldapLogin() 148 if err != nil { 149 log.Warnf("ldap login returned error %v", err) 150 return err 151 } 152 153 // validate parameter 154 attributes, _ := cmd.Flags().GetString("attributes") 155 if attributes == "" { 156 attributes = "*" 157 } 158 159 // lookup target user if given 160 udn := "" 161 if ldapTargetUser != "" { 162 udn, err = lookupTargetUser(lc, ldapTargetUser) 163 if err != nil { 164 log.Errorf("%v", err) 165 return err 166 } 167 if udn != "" { 168 targetDN = udn 169 } 170 } 171 log.Debugf("targetDN:%s", targetDN) 172 173 // search for targetDN entry 174 log.Debugf("ldap search for %s", targetDN) 175 e, err := lc.RetrieveEntry(targetDN, "", attributes) 176 if err != nil { 177 log.Errorf("search for %s returned error %v", targetDN, err) 178 return fmt.Errorf("search for %s returned error %v", targetDN, err) 179 } 180 if e == nil { 181 log.Errorf("ldap search for %s returned no entry", targetDN) 182 return fmt.Errorf("ldap search for %s returned no entry", targetDN) 183 } 184 fmt.Printf("DN '%s' has following attributes:\n", targetDN) 185 values := e.Attributes 186 for _, v := range values { 187 name := v.Name 188 for _, val := range v.Values { 189 log.Infof("%s: %s", name, val) 190 fmt.Printf("%s: %s\n", name, val) 191 } 192 } 193 return nil 194 } 195 196 func promptPassword(label string) (pw string, err error) { 197 prompt := promptui.Prompt{ 198 Label: label, 199 Mask: '*', 200 Stdin: inputReader, 201 } 202 203 result, err := prompt.Run() 204 if err != nil { 205 return 206 } 207 pw = result 208 return 209 } 210 211 func enterNewPassword() (pw string, err error) { 212 pw, err = promptPassword("Enter NEW password") 213 if err != nil { 214 err = fmt.Errorf("error reading password: %v", err) 215 return 216 } 217 log.Debugf("PW1: '%s'", pw) 218 pw2 := "" 219 if !unitTestFlag { 220 pw2, err = promptPassword("Repeat NEW password") 221 } else { 222 // cannot use second promptui in unit tests 223 pw2 = pw 224 } 225 log.Debugf("PW2: '%s'", pw) 226 if err != nil { 227 err = fmt.Errorf("error reading password: %v", err) 228 return 229 } 230 if pw != pw2 { 231 err = fmt.Errorf("passwords do not match") 232 } 233 return 234 } 235 236 func generatePassword(p string) (pw string, err error) { 237 log.Debugf("generated Password") 238 profile, e := setPasswordProfile(p) 239 if e != nil { 240 return "", e 241 } 242 pw, err = pwlib.GenPassword(profile.Length, profile.Upper, profile.Lower, profile.Digits, profile.Special, profile.Firstchar) 243 if err != nil { 244 log.Errorf("password generation returned error %v", err) 245 return 246 } 247 log.Debugf("generated Password: %s", pw) 248 return 249 } 250 251 func getNewPassword(cmd *cobra.Command) (newPassword string, err error) { 252 generate, _ := cmd.Flags().GetBool("generate") 253 if generate { 254 p, _ := cmd.Flags().GetString("profile") 255 newPassword, err = generatePassword(p) 256 if err != nil { 257 return 258 } 259 log.Infof("generated Password: %s", newPassword) 260 fmt.Printf("generated Password: %s\n", newPassword) 261 } else { 262 newPassword, _ = cmd.Flags().GetString("new-password") 263 if newPassword == "" { 264 newPassword = os.Getenv("LDAP_NEW_PASSWORD") 265 if newPassword != "" { 266 log.Debugf("use new password from env: %s", newPassword) 267 } 268 } 269 if newPassword == "" { 270 fmt.Printf("Change password for %s\n", targetDN) 271 newPassword, err = enterNewPassword() 272 if err != nil { 273 return 274 } 275 } 276 } 277 return 278 } 279 func setLdapPass(cmd *cobra.Command, _ []string) error { 280 log.Debugf("ldap password called") 281 // login to server 282 lc, err := ldapLogin() 283 if err != nil { 284 log.Errorf("ldap login returned error %v", err) 285 return err 286 } 287 // lookup target user if given 288 udn := "" 289 if ldapTargetUser != "" { 290 udn, err = lookupTargetUser(lc, ldapTargetUser) 291 if err != nil { 292 log.Errorf("%v", err) 293 return err 294 } 295 if udn != "" { 296 targetDN = udn 297 } 298 } 299 log.Debugf("targetDN: %s", targetDN) 300 301 // validate parameter 302 newPassword := "" 303 newPassword, err = getNewPassword(cmd) 304 if err != nil { 305 return err 306 } 307 if newPassword == "" { 308 // err = fmt.Errorf("no new password given, use --new_password or Env LDAP_NEW_PASSWORD") 309 // return err 310 log.Infof("even no new password given, it will be generated in some systems ldap system such as openldap") 311 } 312 313 // for self write old password must be given und targetDN empty. for admin write targetDN must be given and old password empty 314 oldPass := "" 315 dn := targetDN 316 if targetDN == ldapBindDN { 317 oldPass = ldapBindPassword 318 dn = "" 319 log.Debugf("change password for myself") 320 } 321 // change password 322 genPass := "" 323 genPass, err = lc.SetPassword(dn, oldPass, newPassword) 324 if err != nil { 325 log.Errorf("ldap password change for %s returned error %v", targetDN, err) 326 return fmt.Errorf("ldap password change for %s returned error %v", targetDN, err) 327 } 328 log.Infof("Password for %s changed", targetDN) 329 if genPass == "" { 330 genPass = newPassword 331 } else { 332 log.Infof("generated Password: %s", genPass) 333 fmt.Printf("generated Password: %s\n", genPass) 334 } 335 l := lc.Conn 336 _ = l.Close() 337 338 // reconnect with new password to verify 339 log.Debugf("reconnect with new password to verify") 340 err = lc.Connect(targetDN, genPass) 341 if err != nil { 342 log.Errorf("ldap test bind to %s with new pass returned error %v", targetDN, err) 343 return fmt.Errorf("ldap test bind to %s with new pass returned error %v", targetDN, err) 344 } 345 l = lc.Conn 346 if l != nil { 347 _ = l.Close() 348 } 349 log.Infof("SUCCESS: Password for %s changed and tested", targetDN) 350 fmt.Printf("Password for %s changed and tested\n", targetDN) 351 return nil 352 } 353 354 func setSSHKey(cmd *cobra.Command, _ []string) error { 355 log.Debugf("ldap ssh key called") 356 lc, err := ldapLogin() 357 if err != nil { 358 log.Warnf("ldap login returned error %v", err) 359 return err 360 } 361 362 // lookup target user if given 363 udn := "" 364 if ldapTargetUser != "" { 365 udn, err = lookupTargetUser(lc, ldapTargetUser) 366 if err != nil { 367 log.Errorf("%v", err) 368 return err 369 } 370 if udn != "" { 371 targetDN = udn 372 } 373 } 374 log.Debugf("targetDN: %s", targetDN) 375 // validate parameter 376 sshPubKeyFile, _ := cmd.Flags().GetString("sshpubkeyfile") 377 if sshPubKeyFile == "" { 378 log.Warnf("sshpubkeyfile not given") 379 return fmt.Errorf("sshpubkeyfile not given") 380 } 381 if !common.IsFile(sshPubKeyFile) { 382 log.Warnf("sshpubkeyfile %s not found", sshPubKeyFile) 383 return fmt.Errorf("sshpubkeyfile %s not found", sshPubKeyFile) 384 } 385 386 // read ssh key 387 pubKey := "" 388 pubKey, err = common.ReadFileToString(sshPubKeyFile) 389 if err != nil { 390 log.Warnf("sshpubkeyfile %s not readable", sshPubKeyFile) 391 return fmt.Errorf("sshpubkeyfile %s not readable", sshPubKeyFile) 392 } 393 log.Debugf("got ssh key from file %s", sshPubKeyFile) 394 // search for targetDN entry 395 log.Debugf("ldap exact search for %s", targetDN) 396 l := lc.Conn 397 e, err := lc.RetrieveEntry(targetDN, "", "") 398 if err != nil { 399 log.Errorf("search for %s returned error %v", targetDN, err) 400 return fmt.Errorf("search for %s returned error %v", targetDN, err) 401 } 402 if e == nil { 403 log.Errorf("ldap search for %s returned no entry", targetDN) 404 return fmt.Errorf("ldap search for %s returned no entry", targetDN) 405 } 406 log.Debugf("%s: look for objectclass %s ", targetDN, ldapSSHAttr) 407 // check if attribute is assigned 408 if !ldaplib.HasObjectClass(e, ldapPublicKeyObjectClass) { 409 log.Errorf("objectclass %s not found for %s", ldapPublicKeyObjectClass, targetDN) 410 return fmt.Errorf("objectclass %s not found for %s", ldapPublicKeyObjectClass, targetDN) 411 } 412 413 // check if attribute is already assigned or should added 414 action := "replace" 415 if !ldaplib.HasAttribute(e, ldapSSHAttr) { 416 action = "add" 417 log.Infof("attribute %s not found for %s, will be added", ldapSSHAttr, targetDN) 418 } 419 420 // change or add ssh key 421 log.Debugf("change ssh key for %s", targetDN) 422 err = lc.ModifyAttribute(targetDN, action, ldapSSHAttr, []string{pubKey}) 423 if err != nil { 424 log.Errorf("ldap ssh key change for %s returned error %v", targetDN, err) 425 return fmt.Errorf("ldap ssh key change for %s returned error %v", targetDN, err) 426 } 427 if err = verifySSHKey(lc, pubKey); err != nil { 428 log.Errorf("%v", err) 429 return err 430 } 431 log.Infof("SUCCESS: SSH Key for %s changed", targetDN) 432 fmt.Printf("SSH Key for %s changed\n", targetDN) 433 _ = l.Close() 434 return nil 435 } 436 437 func verifySSHKey(lc *ldaplib.LdapConfigType, pubKey string) (err error) { 438 var e *ldap.Entry 439 // check if ssh key was changed 440 log.Debugf("search for %s attribute %s to verify ssh key", targetDN, ldapSSHAttr) 441 e, err = lc.RetrieveEntry(targetDN, "", ldapSSHAttr) 442 if err != nil { 443 log.Errorf("validate search for %s returned error %v", targetDN, err) 444 return fmt.Errorf("validate search for %s returned error %v", targetDN, err) 445 } 446 actSSH := e.GetAttributeValue(ldapSSHAttr) 447 if actSSH != pubKey { 448 log.Errorf("ldap ssh key change for %s not successful, new value not as expected", targetDN) 449 return fmt.Errorf("ldap ssh key change for %s not successful, new value not as expected", targetDN) 450 } 451 return 452 } 453 func lookupTargetUser(lc *ldaplib.LdapConfigType, user string) (dn string, err error) { 454 log.Debugf("lookup user %s", user) 455 if user == "" { 456 return 457 } 458 if ldapBaseDN == "" { 459 err = fmt.Errorf("no ldap base given") 460 return 461 } 462 filter := fmt.Sprintf("(|(cn=%s)(uid=%s))", user, user) 463 log.Debugf("search with filter %s from %s", user, ldapBaseDN) 464 entries, err := lc.Search(ldapBaseDN, filter, []string{"DN"}, ldap.ScopeWholeSubtree, ldap.DerefInSearching) 465 if err != nil { 466 err = fmt.Errorf("ldap search for user %s returned error %v", user, err) 467 return 468 } 469 l := len(entries) 470 if l == 0 { 471 err = fmt.Errorf("ldap search for user %s returned no entry", user) 472 return 473 } 474 if l > 1 { 475 log.Debugf("search for user %s returned %d entries", user, l) 476 list := make([]string, l) 477 for i, e := range entries { 478 list[i] = e.DN 479 } 480 prompt := promptui.Select{ 481 Label: "Select one of the following entries", 482 Items: list, 483 } 484 485 _, dn, err = prompt.Run() 486 487 if err != nil { 488 fmt.Printf("select entry failed %v\n", err) 489 return 490 } 491 } else { 492 dn = entries[0].DN 493 } 494 log.Debugf("use dn %s for user %s", dn, user) 495 return 496 } 497 498 func init() { 499 ldapCmd.PersistentFlags().StringVarP(&ldapServer, "ldap.host", "H", "", "Hostname of Ldap Server") 500 ldapCmd.PersistentFlags().IntVarP(&ldapPort, "ldap.port", "P", ldapPort, "ldap port to connect") 501 ldapCmd.PersistentFlags().StringVarP(&ldapBaseDN, "ldap.base", "b", "", "Ldap Base DN ") 502 ldapCmd.PersistentFlags().StringVarP(&targetDN, "ldap.targetdn", "T", "", "DN of target User for admin executed password change, empty for own entry (uses LDAP_BIND_DN)") 503 ldapCmd.PersistentFlags().StringVarP(&ldapTargetUser, "ldap.targetuser", "U", "", "uid to search for targetDN") 504 ldapCmd.PersistentFlags().StringVarP(&ldapBindDN, "ldap.binddn", "B", "", "DN of user for LDAP bind or use Env LDAP_BIND_DN") 505 ldapCmd.PersistentFlags().StringVarP(&ldapBindPassword, "ldap.bindpassword", "p", "", "password for LDAP Bind User or use Env LDAP_BIND_PASSWORD") 506 ldapCmd.PersistentFlags().BoolVar(&ldapTLS, "ldap.tls", false, "use secure ldap (ldaps)") 507 ldapCmd.PersistentFlags().BoolVarP(&ldapInsecure, "ldap.insecure", "I", false, "do not verify TLS") 508 ldapCmd.PersistentFlags().IntVarP(&ldapTimeout, "ldap.timeout", "t", ldapTimeout, "ldap timeout in sec") 509 // ldapCmd.SetHelpFunc(hideFlags) 510 ldapPassCmd.Flags().StringP("new-password", "n", "", "new_password to set or use Env LDAP_NEW_PASSWORD or be prompted") 511 ldapPassCmd.Flags().BoolP("generate", "g", false, "generate a new password (alternative to be prompted)") 512 ldapPassCmd.Flags().String("profile", ldapPasswordProfile, "set profile string as numbers of 'length Upper Lower Digits Special FirstcharFlag(0/1)'") 513 ldapPassCmd.MarkFlagsMutuallyExclusive("new-password", "generate") 514 ldapCmd.AddCommand(ldapPassCmd) 515 516 ldapSSHCmd.Flags().StringP("sshpubkeyfile", "f", "id_rsa.pub", "filename with ssh public key to upload") 517 ldapSSHCmd.SetHelpFunc(hideFlags) 518 ldapCmd.AddCommand(ldapSSHCmd) 519 520 ldapShowCmd.Flags().StringP("attributes", "A", "*", "comma separated list of attributes to show") 521 ldapShowCmd.SetHelpFunc(hideFlags) 522 ldapCmd.AddCommand(ldapShowCmd) 523 524 ldapGroupCmd.PersistentFlags().StringVarP(&ldapGroupBase, "ldap.groupbase", "G", "", "Base DN for group search") 525 ldapGroupCmd.SetHelpFunc(hideFlags) 526 ldapCmd.AddCommand(ldapGroupCmd) 527 528 RootCmd.AddCommand(ldapCmd) 529 530 if err := viper.BindPFlags(ldapCmd.PersistentFlags()); err != nil { 531 log.Fatal(err) 532 } 533 } 534 535 func initLdapConfig() { 536 if ldapServer == "" { 537 ldapServer = viper.GetString("ldap.host") 538 } 539 if ldapPort == 0 { 540 ldapPort = viper.GetInt("ldap.port") 541 } 542 if ldapBaseDN == "" { 543 ldapBaseDN = viper.GetString("ldap.base") 544 } 545 if ldapBindDN == "" { 546 ldapBindDN = viper.GetString("ldap.binddn") 547 } 548 if ldapBindPassword == "" { 549 ldapBindPassword = viper.GetString("ldap.bindpassword") 550 } 551 if ldapGroupBase == "" { 552 ldapGroupBase = viper.GetString("ldap.groupbase") 553 } 554 555 if targetDN == "" { 556 targetDN = ldapBindDN 557 } 558 559 if common.CmdFlagChanged(ldapCmd, "ldap.tls") { 560 viper.Set("ldap.tls", ldapTLS) 561 } else { 562 ldapTLS = viper.GetBool("ldap.tls") 563 } 564 if common.CmdFlagChanged(ldapCmd, "ldap.insecure") { 565 viper.Set("ldap.insecure", ldapInsecure) 566 } else { 567 ldapInsecure = viper.GetBool("ldap.insecure") 568 } 569 if common.CmdFlagChanged(ldapCmd, "ldap.timeout") { 570 viper.Set("ldap.timeout", ldapTimeout) 571 } else { 572 ldapTimeout = viper.GetInt("ldap.timeout") 573 } 574 } 575 func loadFromEnv() { 576 if ldapBindDN == "" { 577 ldapBindDN = os.Getenv("LDAP_BIND_DN") 578 if ldapBindDN != "" { 579 log.Debugf("use new LDAP_BIND_DN from env: %s", ldapBindDN) 580 } 581 } 582 if ldapBindPassword == "" { 583 ldapBindPassword = os.Getenv("LDAP_BIND_PASSWORD") 584 if ldapBindPassword != "" { 585 log.Debugf("use LDAP_BIND_PASSWORD from env") 586 } 587 } 588 } 589 func ldapLogin() (lc *ldaplib.LdapConfigType, err error) { 590 initLdapConfig() 591 loadFromEnv() 592 if ldapBindDN == "" { 593 err = fmt.Errorf("no LDAP Bind DN given, use --ldap.binddn or Env LDAP_BIND_DN") 594 return 595 } 596 if ldapBaseDN == "" { 597 p := strings.Split(ldapBindDN, ",") 598 l := len(p) 599 if l > 2 { 600 ldapBaseDN = strings.Join(p[l-2:], ",") 601 log.Debugf("use baseDN from bindDN: %s", ldapBaseDN) 602 } 603 } 604 605 // query password if not given 606 if ldapBindPassword == "" { 607 pw := "" 608 pw, err = promptPassword("Enter Bind password") 609 if err != nil { 610 err = fmt.Errorf("error reading password: %v", err) 611 return 612 } 613 ldapBindPassword = pw 614 } 615 if ldapBindPassword == "" { 616 err = fmt.Errorf("no LDAP Bind Password given, use --ldap.bindpass or Env LDAP_BIND_PASSWORD") 617 return 618 } 619 620 log.Debugf("Try to connect to Ldap Server %s, Port %d, TLS %v, Insecure %v", ldapServer, ldapPort, ldapTLS, ldapInsecure) 621 lc = ldaplib.NewConfig(ldapServer, ldapPort, ldapTLS, ldapInsecure, ldapBaseDN, ldapTimeout) 622 err = lc.Connect(ldapBindDN, ldapBindPassword) 623 if err != nil { 624 err = fmt.Errorf("ldap bind to %s returned error %v", ldapBindDN, err) 625 return 626 } 627 if err == nil && lc.Conn != nil { 628 log.Debugf("Ldap Connected") 629 } 630 return 631 }