You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

admin.go 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687
  1. // Copyright 2016 The Gogs Authors. All rights reserved.
  2. // Copyright 2016 The Gitea Authors. All rights reserved.
  3. // Use of this source code is governed by a MIT-style
  4. // license that can be found in the LICENSE file.
  5. package cmd
  6. import (
  7. "context"
  8. "errors"
  9. "fmt"
  10. "os"
  11. "text/tabwriter"
  12. "code.gitea.io/gitea/models"
  13. "code.gitea.io/gitea/modules/auth/oauth2"
  14. "code.gitea.io/gitea/modules/git"
  15. "code.gitea.io/gitea/modules/graceful"
  16. "code.gitea.io/gitea/modules/log"
  17. pwd "code.gitea.io/gitea/modules/password"
  18. repo_module "code.gitea.io/gitea/modules/repository"
  19. "code.gitea.io/gitea/modules/setting"
  20. "github.com/urfave/cli"
  21. )
  22. var (
  23. // CmdAdmin represents the available admin sub-command.
  24. CmdAdmin = cli.Command{
  25. Name: "admin",
  26. Usage: "Command line interface to perform common administrative operations",
  27. Subcommands: []cli.Command{
  28. subcmdUser,
  29. subcmdRepoSyncReleases,
  30. subcmdRegenerate,
  31. subcmdAuth,
  32. },
  33. }
  34. subcmdUser = cli.Command{
  35. Name: "user",
  36. Usage: "Modify users",
  37. Subcommands: []cli.Command{
  38. microcmdUserCreate,
  39. microcmdUserList,
  40. microcmdUserChangePassword,
  41. microcmdUserDelete,
  42. },
  43. }
  44. microcmdUserList = cli.Command{
  45. Name: "list",
  46. Usage: "List users",
  47. Action: runListUsers,
  48. Flags: []cli.Flag{
  49. cli.BoolFlag{
  50. Name: "admin",
  51. Usage: "List only admin users",
  52. },
  53. },
  54. }
  55. microcmdUserCreate = cli.Command{
  56. Name: "create",
  57. Usage: "Create a new user in database",
  58. Action: runCreateUser,
  59. Flags: []cli.Flag{
  60. cli.StringFlag{
  61. Name: "name",
  62. Usage: "Username. DEPRECATED: use username instead",
  63. },
  64. cli.StringFlag{
  65. Name: "username",
  66. Usage: "Username",
  67. },
  68. cli.StringFlag{
  69. Name: "password",
  70. Usage: "User password",
  71. },
  72. cli.StringFlag{
  73. Name: "email",
  74. Usage: "User email address",
  75. },
  76. cli.BoolFlag{
  77. Name: "admin",
  78. Usage: "User is an admin",
  79. },
  80. cli.BoolFlag{
  81. Name: "random-password",
  82. Usage: "Generate a random password for the user",
  83. },
  84. cli.BoolFlag{
  85. Name: "must-change-password",
  86. Usage: "Set this option to false to prevent forcing the user to change their password after initial login, (Default: true)",
  87. },
  88. cli.IntFlag{
  89. Name: "random-password-length",
  90. Usage: "Length of the random password to be generated",
  91. Value: 12,
  92. },
  93. cli.BoolFlag{
  94. Name: "access-token",
  95. Usage: "Generate access token for the user",
  96. },
  97. },
  98. }
  99. microcmdUserChangePassword = cli.Command{
  100. Name: "change-password",
  101. Usage: "Change a user's password",
  102. Action: runChangePassword,
  103. Flags: []cli.Flag{
  104. cli.StringFlag{
  105. Name: "username,u",
  106. Value: "",
  107. Usage: "The user to change password for",
  108. },
  109. cli.StringFlag{
  110. Name: "password,p",
  111. Value: "",
  112. Usage: "New password to set for user",
  113. },
  114. },
  115. }
  116. microcmdUserDelete = cli.Command{
  117. Name: "delete",
  118. Usage: "Delete specific user",
  119. Flags: []cli.Flag{idFlag},
  120. Action: runDeleteUser,
  121. }
  122. subcmdRepoSyncReleases = cli.Command{
  123. Name: "repo-sync-releases",
  124. Usage: "Synchronize repository releases with tags",
  125. Action: runRepoSyncReleases,
  126. }
  127. subcmdRegenerate = cli.Command{
  128. Name: "regenerate",
  129. Usage: "Regenerate specific files",
  130. Subcommands: []cli.Command{
  131. microcmdRegenHooks,
  132. microcmdRegenKeys,
  133. },
  134. }
  135. microcmdRegenHooks = cli.Command{
  136. Name: "hooks",
  137. Usage: "Regenerate git-hooks",
  138. Action: runRegenerateHooks,
  139. }
  140. microcmdRegenKeys = cli.Command{
  141. Name: "keys",
  142. Usage: "Regenerate authorized_keys file",
  143. Action: runRegenerateKeys,
  144. }
  145. subcmdAuth = cli.Command{
  146. Name: "auth",
  147. Usage: "Modify external auth providers",
  148. Subcommands: []cli.Command{
  149. microcmdAuthAddOauth,
  150. microcmdAuthUpdateOauth,
  151. cmdAuthAddLdapBindDn,
  152. cmdAuthUpdateLdapBindDn,
  153. cmdAuthAddLdapSimpleAuth,
  154. cmdAuthUpdateLdapSimpleAuth,
  155. microcmdAuthList,
  156. microcmdAuthDelete,
  157. },
  158. }
  159. microcmdAuthList = cli.Command{
  160. Name: "list",
  161. Usage: "List auth sources",
  162. Action: runListAuth,
  163. Flags: []cli.Flag{
  164. cli.IntFlag{
  165. Name: "min-width",
  166. Usage: "Minimal cell width including any padding for the formatted table",
  167. Value: 0,
  168. },
  169. cli.IntFlag{
  170. Name: "tab-width",
  171. Usage: "width of tab characters in formatted table (equivalent number of spaces)",
  172. Value: 8,
  173. },
  174. cli.IntFlag{
  175. Name: "padding",
  176. Usage: "padding added to a cell before computing its width",
  177. Value: 1,
  178. },
  179. cli.StringFlag{
  180. Name: "pad-char",
  181. Usage: `ASCII char used for padding if padchar == '\\t', the Writer will assume that the width of a '\\t' in the formatted output is tabwidth, and cells are left-aligned independent of align_left (for correct-looking results, tabwidth must correspond to the tab width in the viewer displaying the result)`,
  182. Value: "\t",
  183. },
  184. cli.BoolFlag{
  185. Name: "vertical-bars",
  186. Usage: "Set to true to print vertical bars between columns",
  187. },
  188. },
  189. }
  190. idFlag = cli.Int64Flag{
  191. Name: "id",
  192. Usage: "ID of authentication source",
  193. }
  194. microcmdAuthDelete = cli.Command{
  195. Name: "delete",
  196. Usage: "Delete specific auth source",
  197. Flags: []cli.Flag{idFlag},
  198. Action: runDeleteAuth,
  199. }
  200. oauthCLIFlags = []cli.Flag{
  201. cli.StringFlag{
  202. Name: "name",
  203. Value: "",
  204. Usage: "Application Name",
  205. },
  206. cli.StringFlag{
  207. Name: "provider",
  208. Value: "",
  209. Usage: "OAuth2 Provider",
  210. },
  211. cli.StringFlag{
  212. Name: "key",
  213. Value: "",
  214. Usage: "Client ID (Key)",
  215. },
  216. cli.StringFlag{
  217. Name: "secret",
  218. Value: "",
  219. Usage: "Client Secret",
  220. },
  221. cli.StringFlag{
  222. Name: "auto-discover-url",
  223. Value: "",
  224. Usage: "OpenID Connect Auto Discovery URL (only required when using OpenID Connect as provider)",
  225. },
  226. cli.StringFlag{
  227. Name: "use-custom-urls",
  228. Value: "false",
  229. Usage: "Use custom URLs for GitLab/GitHub OAuth endpoints",
  230. },
  231. cli.StringFlag{
  232. Name: "custom-auth-url",
  233. Value: "",
  234. Usage: "Use a custom Authorization URL (option for GitLab/GitHub)",
  235. },
  236. cli.StringFlag{
  237. Name: "custom-token-url",
  238. Value: "",
  239. Usage: "Use a custom Token URL (option for GitLab/GitHub)",
  240. },
  241. cli.StringFlag{
  242. Name: "custom-profile-url",
  243. Value: "",
  244. Usage: "Use a custom Profile URL (option for GitLab/GitHub)",
  245. },
  246. cli.StringFlag{
  247. Name: "custom-email-url",
  248. Value: "",
  249. Usage: "Use a custom Email URL (option for GitHub)",
  250. },
  251. }
  252. microcmdAuthUpdateOauth = cli.Command{
  253. Name: "update-oauth",
  254. Usage: "Update existing Oauth authentication source",
  255. Action: runUpdateOauth,
  256. Flags: append(oauthCLIFlags[:1], append([]cli.Flag{idFlag}, oauthCLIFlags[1:]...)...),
  257. }
  258. microcmdAuthAddOauth = cli.Command{
  259. Name: "add-oauth",
  260. Usage: "Add new Oauth authentication source",
  261. Action: runAddOauth,
  262. Flags: oauthCLIFlags,
  263. }
  264. )
  265. func runChangePassword(c *cli.Context) error {
  266. if err := argsSet(c, "username", "password"); err != nil {
  267. return err
  268. }
  269. if err := initDB(); err != nil {
  270. return err
  271. }
  272. if !pwd.IsComplexEnough(c.String("password")) {
  273. return errors.New("Password does not meet complexity requirements")
  274. }
  275. pwned, err := pwd.IsPwned(context.Background(), c.String("password"))
  276. if err != nil {
  277. return err
  278. }
  279. if pwned {
  280. return errors.New("The password you chose is on a list of stolen passwords previously exposed in public data breaches. Please try again with a different password.\nFor more details, see https://haveibeenpwned.com/Passwords")
  281. }
  282. uname := c.String("username")
  283. user, err := models.GetUserByName(uname)
  284. if err != nil {
  285. return err
  286. }
  287. if user.Salt, err = models.GetUserSalt(); err != nil {
  288. return err
  289. }
  290. user.HashPassword(c.String("password"))
  291. if err := models.UpdateUserCols(user, "passwd", "salt"); err != nil {
  292. return err
  293. }
  294. fmt.Printf("%s's password has been successfully updated!\n", user.Name)
  295. return nil
  296. }
  297. func runCreateUser(c *cli.Context) error {
  298. if err := argsSet(c, "email"); err != nil {
  299. return err
  300. }
  301. if c.IsSet("name") && c.IsSet("username") {
  302. return errors.New("Cannot set both --name and --username flags")
  303. }
  304. if !c.IsSet("name") && !c.IsSet("username") {
  305. return errors.New("One of --name or --username flags must be set")
  306. }
  307. if c.IsSet("password") && c.IsSet("random-password") {
  308. return errors.New("cannot set both -random-password and -password flags")
  309. }
  310. var username string
  311. if c.IsSet("username") {
  312. username = c.String("username")
  313. } else {
  314. username = c.String("name")
  315. fmt.Fprintf(os.Stderr, "--name flag is deprecated. Use --username instead.\n")
  316. }
  317. if err := initDB(); err != nil {
  318. return err
  319. }
  320. var password string
  321. if c.IsSet("password") {
  322. password = c.String("password")
  323. } else if c.IsSet("random-password") {
  324. var err error
  325. password, err = pwd.Generate(c.Int("random-password-length"))
  326. if err != nil {
  327. return err
  328. }
  329. fmt.Printf("generated random password is '%s'\n", password)
  330. } else {
  331. return errors.New("must set either password or random-password flag")
  332. }
  333. // always default to true
  334. var changePassword = true
  335. // If this is the first user being created.
  336. // Take it as the admin and don't force a password update.
  337. if n := models.CountUsers(); n == 0 {
  338. changePassword = false
  339. }
  340. if c.IsSet("must-change-password") {
  341. changePassword = c.Bool("must-change-password")
  342. }
  343. u := &models.User{
  344. Name: username,
  345. Email: c.String("email"),
  346. Passwd: password,
  347. IsActive: true,
  348. IsAdmin: c.Bool("admin"),
  349. MustChangePassword: changePassword,
  350. Theme: setting.UI.DefaultTheme,
  351. }
  352. if err := models.CreateUser(u); err != nil {
  353. return fmt.Errorf("CreateUser: %v", err)
  354. }
  355. if c.Bool("access-token") {
  356. t := &models.AccessToken{
  357. Name: "gitea-admin",
  358. UID: u.ID,
  359. }
  360. if err := models.NewAccessToken(t); err != nil {
  361. return err
  362. }
  363. fmt.Printf("Access token was successfully created... %s\n", t.Token)
  364. }
  365. fmt.Printf("New user '%s' has been successfully created!\n", username)
  366. return nil
  367. }
  368. func runListUsers(c *cli.Context) error {
  369. if err := initDB(); err != nil {
  370. return err
  371. }
  372. users, err := models.GetAllUsers()
  373. if err != nil {
  374. return err
  375. }
  376. w := tabwriter.NewWriter(os.Stdout, 5, 0, 1, ' ', 0)
  377. if c.IsSet("admin") {
  378. fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\n")
  379. for _, u := range users {
  380. if u.IsAdmin {
  381. fmt.Fprintf(w, "%d\t%s\t%s\t%t\n", u.ID, u.Name, u.Email, u.IsActive)
  382. }
  383. }
  384. } else {
  385. fmt.Fprintf(w, "ID\tUsername\tEmail\tIsActive\tIsAdmin\n")
  386. for _, u := range users {
  387. fmt.Fprintf(w, "%d\t%s\t%s\t%t\t%t\n", u.ID, u.Name, u.Email, u.IsActive, u.IsAdmin)
  388. }
  389. }
  390. w.Flush()
  391. return nil
  392. }
  393. func runDeleteUser(c *cli.Context) error {
  394. if !c.IsSet("id") {
  395. return fmt.Errorf("--id flag is missing")
  396. }
  397. if err := initDB(); err != nil {
  398. return err
  399. }
  400. user, err := models.GetUserByID(c.Int64("id"))
  401. if err != nil {
  402. return err
  403. }
  404. return models.DeleteUser(user)
  405. }
  406. func runRepoSyncReleases(c *cli.Context) error {
  407. if err := initDB(); err != nil {
  408. return err
  409. }
  410. log.Trace("Synchronizing repository releases (this may take a while)")
  411. for page := 1; ; page++ {
  412. repos, count, err := models.SearchRepositoryByName(&models.SearchRepoOptions{
  413. ListOptions: models.ListOptions{
  414. PageSize: models.RepositoryListDefaultPageSize,
  415. Page: page,
  416. },
  417. Private: true,
  418. })
  419. if err != nil {
  420. return fmt.Errorf("SearchRepositoryByName: %v", err)
  421. }
  422. if len(repos) == 0 {
  423. break
  424. }
  425. log.Trace("Processing next %d repos of %d", len(repos), count)
  426. for _, repo := range repos {
  427. log.Trace("Synchronizing repo %s with path %s", repo.FullName(), repo.RepoPath())
  428. gitRepo, err := git.OpenRepository(repo.RepoPath())
  429. if err != nil {
  430. log.Warn("OpenRepository: %v", err)
  431. continue
  432. }
  433. oldnum, err := getReleaseCount(repo.ID)
  434. if err != nil {
  435. log.Warn(" GetReleaseCountByRepoID: %v", err)
  436. }
  437. log.Trace(" currentNumReleases is %d, running SyncReleasesWithTags", oldnum)
  438. if err = repo_module.SyncReleasesWithTags(repo, gitRepo); err != nil {
  439. log.Warn(" SyncReleasesWithTags: %v", err)
  440. gitRepo.Close()
  441. continue
  442. }
  443. count, err = getReleaseCount(repo.ID)
  444. if err != nil {
  445. log.Warn(" GetReleaseCountByRepoID: %v", err)
  446. gitRepo.Close()
  447. continue
  448. }
  449. log.Trace(" repo %s releases synchronized to tags: from %d to %d",
  450. repo.FullName(), oldnum, count)
  451. gitRepo.Close()
  452. }
  453. }
  454. return nil
  455. }
  456. func getReleaseCount(id int64) (int64, error) {
  457. return models.GetReleaseCountByRepoID(
  458. id,
  459. models.FindReleasesOptions{
  460. IncludeTags: true,
  461. },
  462. )
  463. }
  464. func runRegenerateHooks(c *cli.Context) error {
  465. if err := initDB(); err != nil {
  466. return err
  467. }
  468. return repo_module.SyncRepositoryHooks(graceful.GetManager().ShutdownContext())
  469. }
  470. func runRegenerateKeys(c *cli.Context) error {
  471. if err := initDB(); err != nil {
  472. return err
  473. }
  474. return models.RewriteAllPublicKeys()
  475. }
  476. func parseOAuth2Config(c *cli.Context) *models.OAuth2Config {
  477. var customURLMapping *oauth2.CustomURLMapping
  478. if c.IsSet("use-custom-urls") {
  479. customURLMapping = &oauth2.CustomURLMapping{
  480. TokenURL: c.String("custom-token-url"),
  481. AuthURL: c.String("custom-auth-url"),
  482. ProfileURL: c.String("custom-profile-url"),
  483. EmailURL: c.String("custom-email-url"),
  484. }
  485. } else {
  486. customURLMapping = nil
  487. }
  488. return &models.OAuth2Config{
  489. Provider: c.String("provider"),
  490. ClientID: c.String("key"),
  491. ClientSecret: c.String("secret"),
  492. OpenIDConnectAutoDiscoveryURL: c.String("auto-discover-url"),
  493. CustomURLMapping: customURLMapping,
  494. }
  495. }
  496. func runAddOauth(c *cli.Context) error {
  497. if err := initDB(); err != nil {
  498. return err
  499. }
  500. return models.CreateLoginSource(&models.LoginSource{
  501. Type: models.LoginOAuth2,
  502. Name: c.String("name"),
  503. IsActived: true,
  504. Cfg: parseOAuth2Config(c),
  505. })
  506. }
  507. func runUpdateOauth(c *cli.Context) error {
  508. if !c.IsSet("id") {
  509. return fmt.Errorf("--id flag is missing")
  510. }
  511. if err := initDB(); err != nil {
  512. return err
  513. }
  514. source, err := models.GetLoginSourceByID(c.Int64("id"))
  515. if err != nil {
  516. return err
  517. }
  518. oAuth2Config := source.OAuth2()
  519. if c.IsSet("name") {
  520. source.Name = c.String("name")
  521. }
  522. if c.IsSet("provider") {
  523. oAuth2Config.Provider = c.String("provider")
  524. }
  525. if c.IsSet("key") {
  526. oAuth2Config.ClientID = c.String("key")
  527. }
  528. if c.IsSet("secret") {
  529. oAuth2Config.ClientSecret = c.String("secret")
  530. }
  531. if c.IsSet("auto-discover-url") {
  532. oAuth2Config.OpenIDConnectAutoDiscoveryURL = c.String("auto-discover-url")
  533. }
  534. // update custom URL mapping
  535. var customURLMapping = &oauth2.CustomURLMapping{}
  536. if oAuth2Config.CustomURLMapping != nil {
  537. customURLMapping.TokenURL = oAuth2Config.CustomURLMapping.TokenURL
  538. customURLMapping.AuthURL = oAuth2Config.CustomURLMapping.AuthURL
  539. customURLMapping.ProfileURL = oAuth2Config.CustomURLMapping.ProfileURL
  540. customURLMapping.EmailURL = oAuth2Config.CustomURLMapping.EmailURL
  541. }
  542. if c.IsSet("use-custom-urls") && c.IsSet("custom-token-url") {
  543. customURLMapping.TokenURL = c.String("custom-token-url")
  544. }
  545. if c.IsSet("use-custom-urls") && c.IsSet("custom-auth-url") {
  546. customURLMapping.AuthURL = c.String("custom-auth-url")
  547. }
  548. if c.IsSet("use-custom-urls") && c.IsSet("custom-profile-url") {
  549. customURLMapping.ProfileURL = c.String("custom-profile-url")
  550. }
  551. if c.IsSet("use-custom-urls") && c.IsSet("custom-email-url") {
  552. customURLMapping.EmailURL = c.String("custom-email-url")
  553. }
  554. oAuth2Config.CustomURLMapping = customURLMapping
  555. source.Cfg = oAuth2Config
  556. return models.UpdateSource(source)
  557. }
  558. func runListAuth(c *cli.Context) error {
  559. if err := initDB(); err != nil {
  560. return err
  561. }
  562. loginSources, err := models.LoginSources()
  563. if err != nil {
  564. return err
  565. }
  566. flags := tabwriter.AlignRight
  567. if c.Bool("vertical-bars") {
  568. flags |= tabwriter.Debug
  569. }
  570. padChar := byte('\t')
  571. if len(c.String("pad-char")) > 0 {
  572. padChar = c.String("pad-char")[0]
  573. }
  574. // loop through each source and print
  575. w := tabwriter.NewWriter(os.Stdout, c.Int("min-width"), c.Int("tab-width"), c.Int("padding"), padChar, flags)
  576. fmt.Fprintf(w, "ID\tName\tType\tEnabled\n")
  577. for _, source := range loginSources {
  578. fmt.Fprintf(w, "%d\t%s\t%s\t%t\n", source.ID, source.Name, models.LoginNames[source.Type], source.IsActived)
  579. }
  580. w.Flush()
  581. return nil
  582. }
  583. func runDeleteAuth(c *cli.Context) error {
  584. if !c.IsSet("id") {
  585. return fmt.Errorf("--id flag is missing")
  586. }
  587. if err := initDB(); err != nil {
  588. return err
  589. }
  590. source, err := models.GetLoginSourceByID(c.Int64("id"))
  591. if err != nil {
  592. return err
  593. }
  594. return models.DeleteSource(source)
  595. }