main.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635
  1. /*
  2. * swsyd - swsy daemon, make nsswitch easy
  3. * Copyright (C) 2024 Marcus Pedersén marcus@marcux.org
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU General Public License as published by
  7. * the Free Software Foundation, either version 3 of the License, or
  8. * (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU General Public License
  16. * along with this program. If not, see <http://www.gnu.org/licenses/>.
  17. */
  18. package main
  19. import (
  20. "crypto/rand"
  21. "os"
  22. "fmt"
  23. "log"
  24. "log/slog"
  25. "strconv"
  26. "strings"
  27. "regexp"
  28. "net"
  29. "net/http"
  30. "path/filepath"
  31. "gorm.io/gorm"
  32. "github.com/gin-gonic/gin"
  33. "github.com/tredoe/osutil/user/crypt/sha512_crypt"
  34. "notabug.org/marcux/swsy/swsyd/common"
  35. "notabug.org/marcux/swsy/swsyd/sql"
  36. )
  37. const VERSION string = "0.0.1"
  38. const AUTHORS string = "Marcus Pedersén"
  39. // Response message on post request
  40. type PostResponse struct {
  41. Message string
  42. }
  43. // Struct holds handle
  44. // to database and
  45. // describes the API
  46. type swsydAPI struct {
  47. DB *gorm.DB
  48. conf common.Config
  49. }
  50. // Returns users matching
  51. // url query parameters
  52. // name or uid. If both
  53. // are given uid will be
  54. // used. If name is empty
  55. // string and search is specified
  56. // all users are
  57. // returned, same as no
  58. // parameters.
  59. // If search is not specified
  60. // and name is empty empty array
  61. // is returned. Name with no search
  62. // the exact matching user is returned
  63. // if no user is found empty array
  64. // is returned.
  65. // API:
  66. // GET URL: /user
  67. // Get all users
  68. // GET URL: /user?uid=123
  69. // Get user with uid 123
  70. // GET URL: /user?name=abc
  71. // Get user with name abc
  72. // URL: /user?name=abc&search
  73. // Get users starting with abc
  74. func (s swsydAPI) getUser(c *gin.Context) {
  75. name, exName := c.GetQuery("name")
  76. uid, exUid := c.GetQuery("uid")
  77. _, exSearch := c.GetQuery("search")
  78. if exUid {
  79. // Get user with uid
  80. u, err := strconv.Atoi(uid)
  81. if err != nil {
  82. slog.Error(fmt.Sprintf("Failed to convert string: %s to int: %s", uid, err))
  83. users := []sql.Password{}
  84. c.JSON(http.StatusOK, users)
  85. return
  86. }
  87. users := sql.GetUser(s.DB, "", int32(u), false)
  88. c.JSON(http.StatusOK, users)
  89. } else if exName {
  90. // Get user with exact name if not search
  91. // Get user with name like if search
  92. users := sql.GetUser(s.DB, name, -1, exSearch)
  93. c.JSON(http.StatusOK, users)
  94. return
  95. } else {
  96. // Get all users
  97. users := sql.GetUser(s.DB, "", -1, true)
  98. c.JSON(http.StatusOK, users)
  99. }
  100. }
  101. // API:
  102. // GET URL: /group
  103. // Get all groups
  104. // GET URL: /group?gid=123
  105. // Get group with gid 123
  106. // GET URL: /group?name=abc
  107. // Get group with name abc
  108. // GET URL: /group?name=abc&search
  109. // Get groups starting with abc
  110. // GET URL: /group?user=abc
  111. // Get all groups that user with username
  112. // belongs to
  113. // GET URL: /group?uid=123
  114. // Get all groups that user with uid
  115. // belongs to.
  116. // Any other parameter combinations
  117. // returns an empty array.
  118. func (s swsydAPI) getGroup(c *gin.Context) {
  119. name, exName := c.GetQuery("name")
  120. gid, exGid := c.GetQuery("gid")
  121. _ , exSearch := c.GetQuery("search")
  122. uid, exUid := c.GetQuery("uid")
  123. user, exUser := c.GetQuery("user")
  124. if ! exName && ! exGid && ! exSearch && ! exUid && ! exUser {
  125. // Get all groups
  126. fmt.Println("getting all groups")
  127. groups := sql.GetGroup(s.DB, "", -1, true)
  128. c.JSON(http.StatusOK, groups)
  129. } else if exName && ! exGid && ! exUid && ! exUser {
  130. // Get group with name
  131. groups := sql.GetGroup(s.DB, name, -1, exSearch)
  132. c.JSON(http.StatusOK, groups)
  133. } else if exGid && ! exName && ! exSearch && ! exUid && ! exUser {
  134. // Get group with gid
  135. igid, err := strconv.Atoi(gid)
  136. if err != nil {
  137. slog.Error(fmt.Sprintf("getGroup: Failed to convert gid: %s to int. %s\n", gid, err))
  138. c.JSON(http.StatusOK, []sql.Group{})
  139. }
  140. groups := sql.GetGroup(s.DB, "", int32(igid), false)
  141. c.JSON(http.StatusOK, groups)
  142. } else if exUid && ! exGid && ! exSearch && ! exName && ! exUser {
  143. // Get groups for user with uid
  144. iuid, err := strconv.Atoi(uid)
  145. if err != nil {
  146. slog.Error(fmt.Sprintf("getGroup: Failed to convert uid: %s to int. %s\n", uid, err))
  147. c.JSON(http.StatusOK, []sql.Group{})
  148. } else {
  149. groups := sql.GetUserGroups(s.DB, "", int32(iuid))
  150. c.JSON(http.StatusOK, groups)
  151. }
  152. } else if exUser && ! exGid && ! exSearch && ! exUid && ! exName {
  153. // Get groups for username
  154. groups := sql.GetUserGroups(s.DB, user, -1)
  155. c.JSON(http.StatusOK, groups)
  156. } else {
  157. c.JSON(http.StatusOK, []sql.Group{})
  158. }
  159. }
  160. // API:
  161. // GET URL: /shadow
  162. // Get all shadow fields
  163. // GET URL: /shadow?user=abc
  164. // Get shadow fields for user
  165. // with username
  166. // GET URL: /shadow?user=abc&search
  167. // Get shadow fields for users
  168. // starting with name abc
  169. // GET URL: /shadow?uid=123
  170. // Get shadow fields for user with uid
  171. // Any other parameter combinations
  172. // returns an empty array.
  173. func (s swsydAPI) getShadow(c *gin.Context) {
  174. uid, exUid := c.GetQuery("uid")
  175. user, exUser := c.GetQuery("user")
  176. _ , exSearch := c.GetQuery("search")
  177. shadows := []sql.Shadow{}
  178. if !exUid && !exUser && !exSearch {
  179. // Get all shadow records
  180. shadows = sql.GetShadow(s.DB, "", -1, true)
  181. } else if exUid && !exUser && !exSearch {
  182. // Get shafow fields for user with uid
  183. iuid, err := strconv.Atoi(uid)
  184. if err != nil {
  185. slog.Error(fmt.Sprintf("GetShadow: Failed to convert uid string: %s to int. %s", uid, err))
  186. } else {
  187. shadows = sql.GetShadow(s.DB, "", int32(iuid), exSearch)
  188. }
  189. } else if exUser && !exUid {
  190. // Get shadow with username or username staring with user
  191. shadows = sql.GetShadow(s.DB, user, -1, exSearch)
  192. }
  193. c.JSON(http.StatusOK, shadows)
  194. }
  195. // API:
  196. // GET URL: /host
  197. // Get all hosts
  198. // GET URL: /host?name=abc
  199. // Get host with hostname name
  200. // GET URL: /host?name=abc&search
  201. // Get host weith hostname
  202. // starting with name abc
  203. // GET URL: /host?ipv4=1.2.3.4
  204. // Get host with IP version 4 number
  205. // GET URL: /host?ipv6=1234:fd2:5621:1:89::4500
  206. // Get host with IP version 6 number
  207. // Any other parameter combinations
  208. // returns an empty array.
  209. func (s swsydAPI) getHost(c *gin.Context) {
  210. name, exName := c.GetQuery("name")
  211. ipv4, exIpv4 := c.GetQuery("ipv4")
  212. ipv6, exIpv6 := c.GetQuery("ipv6")
  213. _ , exSearch := c.GetQuery("search")
  214. hosts := []sql.Host{}
  215. if !exName && !exSearch && !exIpv4 && !exIpv6 {
  216. // Get all hosts
  217. hosts = sql.GetHost(s.DB, "", "", "", true)
  218. } else if exName && !exSearch && !exIpv4 && !exIpv6{
  219. // Get host with name
  220. hosts = sql.GetHost(s.DB, name, "", "", false)
  221. } else if exName && exSearch && !exIpv4 && !exIpv6 {
  222. // Get host starting with name
  223. hosts = sql.GetHost(s.DB, name, "", "", true)
  224. } else if exIpv4 && !exIpv6 && !exName && !exSearch {
  225. // Get host with IPv4
  226. hosts = sql.GetHost(s.DB, "", ipv4, "", false)
  227. } else if exIpv6 && !exIpv4 && !exName && !exSearch {
  228. // Get host with IPv6
  229. hosts = sql.GetHost(s.DB, "", "", ipv6, false)
  230. }
  231. c.JSON(http.StatusOK, hosts)
  232. }
  233. // Add user
  234. // API:
  235. // POST URL: /user
  236. // With the following form fields:
  237. // username: Required string
  238. // uid: optional positive integer
  239. // gid: optional positive integer
  240. // gecos: optional string
  241. // home: optional string (path)
  242. // shell: optional string (path to shell)
  243. func (s swsydAPI) postUser(c *gin.Context) {
  244. response := PostResponse{}
  245. u := sql.Password{}
  246. err := c.Bind(&u)
  247. if err != nil {
  248. slog.Error(fmt.Sprintf("postUser: Failed to parse supplied form to user: %s", err))
  249. response.Message = fmt.Sprintf("Failed to parse supplied form: %s", err)
  250. c.JSON(http.StatusBadRequest, response)
  251. } else if len(u.Username) == 0 {
  252. response.Message = "username is required"
  253. c.JSON(http.StatusBadRequest, response)
  254. }
  255. if len(u.Home) == 0 {
  256. u.Home = filepath.Join(s.conf.DefaultHome, u.Username)
  257. }
  258. if len(u.Shell) == 0 {
  259. u.Shell = s.conf.DefaultShell
  260. }
  261. if len(u.Password) != 0 {
  262. u.Password = ""
  263. }
  264. if err := sql.InsertUser(s.DB, s.conf, u); err != nil {
  265. response.Message = fmt.Sprintf("%s",err)
  266. c.JSON(http.StatusInternalServerError, response)
  267. }
  268. response.Message = fmt.Sprintf("User: %s successfully created", u.Username)
  269. c.JSON(http.StatusCreated, response)
  270. }
  271. // Add group
  272. // API:
  273. // POST URL: /group
  274. // With the following form fields:
  275. // groupname: Required string
  276. // gid: optional positive integer
  277. // password: clear text string
  278. func (s swsydAPI) postGroup(c *gin.Context) {
  279. response := PostResponse{}
  280. g := sql.Group{}
  281. err := c.Bind(&g)
  282. if err != nil {
  283. slog.Error(fmt.Sprintf("postGroup: Failed to parse group from form: %s", err))
  284. response.Message = fmt.Sprintf("Failed to parse form: %s", err)
  285. c.JSON(http.StatusBadRequest, response)
  286. return
  287. } else if len(g.Groupname) == 0 {
  288. response.Message = "groupname is required"
  289. c.JSON(http.StatusBadRequest, response)
  290. return
  291. }
  292. if len(g.GroupPassword) > 0 {
  293. hash, err := passwordSha512(g.GroupPassword)
  294. if err != nil {
  295. slog.Error(fmt.Sprintf("%s", err))
  296. response.Message = "Failed to hash password"
  297. c.JSON(http.StatusInternalServerError, response)
  298. return
  299. } else {
  300. g.GroupPassword = hash
  301. }
  302. }
  303. err = sql.InsertGroup(s.DB, s.conf, g)
  304. if err != nil {
  305. slog.Error("Failed to save group: %s, to database: %s", g, err)
  306. response.Message = fmt.Sprintf("%s", err)
  307. c.JSON(http.StatusInternalServerError, response)
  308. return
  309. }
  310. response.Message = "Group successfully added."
  311. c.JSON(http.StatusOK, response)
  312. }
  313. // Add swadow
  314. // API:
  315. // POST URL: /shadow
  316. // With the following form fields:
  317. // username: Required string if not UID
  318. // uid: Required integer if not username
  319. // password: clear text string
  320. // lastchange: optional integer
  321. // minpasswordage: optional integer
  322. // maxpasswordage: optional integer
  323. // warningperiod: optional integer
  324. // inactiveperiod: optional integer
  325. // expiredate: string formated as date
  326. func (s swsydAPI) postShadow(c *gin.Context) {
  327. response := PostResponse{}
  328. sh := sql.Shadow{}
  329. uid := -1
  330. uidStr := c.PostForm("uid")
  331. err := c.Bind(&sh)
  332. if err != nil {
  333. slog.Error(fmt.Sprintf("postShadow: Failed to parse shadow from form: %s", err))
  334. response.Message = fmt.Sprintf("Failed to parse form: %s", err)
  335. c.JSON(http.StatusBadRequest, response)
  336. return
  337. } else if len(sh.Username) == 0 && len(uidStr) == 0 {
  338. response.Message = "username or uid is required"
  339. c.JSON(http.StatusBadRequest, response)
  340. return
  341. }
  342. if len(uidStr) > 0 {
  343. uid, err = strconv.Atoi(uidStr)
  344. if err != nil {
  345. slog.Error(fmt.Sprintf("postShadow: Failed to convert uid: %s to int: %s", uidStr, err))
  346. response.Message = fmt.Sprintf("Failed to convert uid: %s to integer.", uidStr)
  347. c.JSON(http.StatusBadRequest, response)
  348. return
  349. }
  350. }
  351. if len(sh.Password) > 0 {
  352. hash, err := passwordSha512(sh.Password)
  353. if err != nil {
  354. slog.Error(fmt.Sprintf("postShadow: %s", err))
  355. response.Message = "Failed to hash password"
  356. c.JSON(http.StatusInternalServerError, response)
  357. return
  358. } else {
  359. sh.Password = hash
  360. }
  361. }
  362. err = sql.InsertShadow(s.DB, s.conf, sh, int32(uid))
  363. if err != nil {
  364. slog.Error("Failed to save shadow: %s, to database: %s", sh, err)
  365. response.Message = fmt.Sprintf("%s", err)
  366. c.JSON(http.StatusInternalServerError, response)
  367. return
  368. }
  369. response.Message = "Password successfully added."
  370. c.JSON(http.StatusOK, response)
  371. }
  372. // Add host
  373. // API:
  374. // POST URL: /host
  375. // With the following form fields:
  376. // ipv4: IPv4 address, eg 192.168.1.16
  377. // ipv6: IPv6 address
  378. // hostnames: string with space separated hostnames, required
  379. func (s swsydAPI) postHost(c *gin.Context) {
  380. response := PostResponse{}
  381. h := sql.Host{}
  382. err := c.Bind(&h)
  383. if err != nil {
  384. slog.Error(fmt.Sprintf("postHost: Failed to parse host from form: %s", err))
  385. response.Message = fmt.Sprintf("Failed to parse form: %s", err)
  386. c.JSON(http.StatusBadRequest, response)
  387. return
  388. }
  389. if len(h.Hostnames) == 0 {
  390. slog.Error("postHost: hostnames is required")
  391. response.Message = "Failed to add host, hostnames is required."
  392. c.JSON(http.StatusBadRequest, response)
  393. return
  394. }
  395. if len(h.IPv4) > 0 {
  396. ip4 := net.ParseIP(h.IPv4)
  397. if ip4 == nil {
  398. slog.Error(fmt.Sprintf("postHost: IPv4: %s, is not a valid IP address", h.IPv4))
  399. response.Message = fmt.Sprintf("Failed to add host, IPv4: %s is not a valid IP address.", h.IPv4)
  400. c.JSON(http.StatusBadRequest, response)
  401. return
  402. }
  403. }
  404. if len(h.IPv6) > 0 {
  405. ipv6 := net.ParseIP(h.IPv6)
  406. if ipv6 == nil {
  407. slog.Error(fmt.Sprintf("postHost: IPv6: %s, is not a valid IP address", h.IPv6))
  408. response.Message = fmt.Sprintf("Failed to add host, IPv6; %s is not a valid IP address", h.IPv6)
  409. c.JSON(http.StatusBadRequest, response)
  410. return
  411. }
  412. }
  413. h.Hostnames = strings.TrimSpace(h.Hostnames)
  414. regNames := regexp.MustCompile("^[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]$")
  415. // TODO
  416. // clean up muliple hosts, remove multiple spaces between names
  417. hostnames := ""
  418. for _, n := range strings.Fields(h.Hostnames) {
  419. if ! regNames.MatchString(n) {
  420. slog.Error("postHost: Hostnasme: %s is an invalid hostname", n)
  421. response.Message = fmt.Sprintf("Hostname: %s is invalid, name must start and end with alpanumeric characters and name can only contain \"-\", \".\" and alpanumeric characters.", n)
  422. c.JSON(http.StatusBadRequest, response)
  423. return
  424. }
  425. if len(hostnames) == 0 {
  426. hostnames = n
  427. } else {
  428. hostnames = fmt.Sprintf("%s %s", hostnames, n)
  429. }
  430. }
  431. h.Hostnames = hostnames
  432. err = sql.InsertHost(s.DB, s.conf, h)
  433. if err != nil {
  434. slog.Error(fmt.Sprintf("postHost: Failed to save host: %+v to database: %s", h, err))
  435. response.Message = fmt.Sprintf("%s", err)
  436. c.JSON(http.StatusInternalServerError, response)
  437. return
  438. }
  439. response.Message = "Host succesfully added."
  440. c.JSON(http.StatusOK, response)
  441. }
  442. // Hash password according to
  443. // unix shadow file
  444. // sha512
  445. func passwordSha512(pwd string) (string, error) {
  446. const saltSize = 16
  447. var salt = make([]byte, saltSize)
  448. _, err := rand.Read(salt[:])
  449. if err != nil {
  450. return "", fmt.Errorf("Failed to generate salt for sha512 hash password: %s", err)
  451. }
  452. salt = []byte(fmt.Sprintf("$6$%x", salt))
  453. c := sha512_crypt.New()
  454. hash, err := c.Generate([]byte(pwd), salt)
  455. if err != nil {
  456. return "", fmt.Errorf("Failed to generate password hash sha512")
  457. }
  458. return string(hash), nil
  459. }
  460. // Prints version to stdout
  461. func printVersion() {
  462. fmt.Println("swsyd", VERSION)
  463. fmt.Printf("Copyright (C) 2024 %s.\n", AUTHORS)
  464. fmt.Println("License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>")
  465. fmt.Println("This is free software: you are free to change and redistribute it.")
  466. fmt.Println("There is NO WARRANTY, to the extent permitted by law.")
  467. fmt.Println("")
  468. fmt.Printf("Written by %s.\n", AUTHORS)
  469. }
  470. // Prints help text to stdout
  471. func printHelp() {
  472. fmt.Println("Usage: swsyd [OPTION]...")
  473. fmt.Println("swsy daemon, make nsswitch easy")
  474. fmt.Println("")
  475. fmt.Println("Without any arguments swsyd will start in daemon mode.")
  476. fmt.Println("Deamon responds both to nsswitch clients and admin")
  477. fmt.Println("client that manages users, groups and hosts.")
  478. fmt.Println("")
  479. fmt.Println("Options:")
  480. fmt.Println(" -h, --help Prints this helptext and exit.")
  481. fmt.Println(" -V, --version Prints version information and exit.")
  482. }
  483. // Initialize log
  484. func initLog(c common.Config) {
  485. logHandle, err := os.OpenFile(c.Logfile,
  486. os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
  487. if err != nil {
  488. fmt.Fprintf(os.Stderr, "Failed to open logfile: %s, printing log to Stdout.\n%s\n",
  489. c.Logfile, err)
  490. } else {
  491. log.SetOutput(logHandle)
  492. }
  493. slog.SetLogLoggerLevel(c.Loglevel)
  494. }
  495. // Program start
  496. func main() {
  497. if len(os.Args) >= 3 {
  498. fmt.Fprintf(os.Stderr, "Wrong arguments.\nTry -h or --help for help.\n")
  499. os.Exit(1)
  500. } else if len(os.Args) == 2 {
  501. if os.Args[1] == "--version" || os.Args[1] == "-V" {
  502. printVersion()
  503. os.Exit(0)
  504. } else if os.Args[1] == "--help" || os.Args[1] == "-h" {
  505. printHelp()
  506. os.Exit(0)
  507. } else {
  508. fmt.Fprintf(os.Stderr, "Wrong arguments.\nTry -h or --help for help.\n")
  509. os.Exit(1)
  510. }
  511. }
  512. conf := common.GetConfig()
  513. initLog(conf)
  514. slog.Info("Starting swsyd")
  515. swsyd := swsydAPI{}
  516. db, err := sql.InitDb(conf)
  517. if err != nil {
  518. slog.Error(fmt.Sprintf("Failed to initialize database: %s", conf.Dbfile))
  519. slog.Error(fmt.Sprintf("%s", err))
  520. slog.Error("Terminating swsyd")
  521. os.Exit(3)
  522. }
  523. swsyd.conf = conf
  524. swsyd.DB = db
  525. defer func() {
  526. if db, err := swsyd.DB.DB(); err != nil {
  527. slog.Error("Failed to close database connection:")
  528. slog.Error(fmt.Sprintf("%s", err))
  529. } else {
  530. db.Close()
  531. }
  532. }()
  533. g := gin.Default()
  534. g.GET("/user", swsyd.getUser)
  535. g.GET("/group", swsyd.getGroup)
  536. g.GET("/shadow", swsyd.getShadow)
  537. g.GET("/host",swsyd.getHost)
  538. g.POST("/user", swsyd.postUser)
  539. g.POST("/group", swsyd.postGroup)
  540. g.POST("/shadow", swsyd.postShadow)
  541. g.POST("/host", swsyd.postHost)
  542. g.Run(fmt.Sprintf(":%d", conf.Port))
  543. }