123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635 |
- /*
- * swsyd - swsy daemon, make nsswitch easy
- * Copyright (C) 2024 Marcus Pedersén marcus@marcux.org
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License as published by
- * the Free Software Foundation, either version 3 of the License, or
- * (at your option) any later version.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see <http://www.gnu.org/licenses/>.
- */
- package main
- import (
- "crypto/rand"
- "os"
- "fmt"
- "log"
- "log/slog"
- "strconv"
- "strings"
- "regexp"
- "net"
- "net/http"
- "path/filepath"
- "gorm.io/gorm"
- "github.com/gin-gonic/gin"
- "github.com/tredoe/osutil/user/crypt/sha512_crypt"
- "notabug.org/marcux/swsy/swsyd/common"
- "notabug.org/marcux/swsy/swsyd/sql"
- )
- const VERSION string = "0.0.1"
- const AUTHORS string = "Marcus Pedersén"
- // Response message on post request
- type PostResponse struct {
- Message string
- }
- // Struct holds handle
- // to database and
- // describes the API
- type swsydAPI struct {
- DB *gorm.DB
- conf common.Config
- }
- // Returns users matching
- // url query parameters
- // name or uid. If both
- // are given uid will be
- // used. If name is empty
- // string and search is specified
- // all users are
- // returned, same as no
- // parameters.
- // If search is not specified
- // and name is empty empty array
- // is returned. Name with no search
- // the exact matching user is returned
- // if no user is found empty array
- // is returned.
- // API:
- // GET URL: /user
- // Get all users
- // GET URL: /user?uid=123
- // Get user with uid 123
- // GET URL: /user?name=abc
- // Get user with name abc
- // URL: /user?name=abc&search
- // Get users starting with abc
- func (s swsydAPI) getUser(c *gin.Context) {
- name, exName := c.GetQuery("name")
- uid, exUid := c.GetQuery("uid")
- _, exSearch := c.GetQuery("search")
- if exUid {
- // Get user with uid
- u, err := strconv.Atoi(uid)
- if err != nil {
- slog.Error(fmt.Sprintf("Failed to convert string: %s to int: %s", uid, err))
- users := []sql.Password{}
- c.JSON(http.StatusOK, users)
- return
- }
- users := sql.GetUser(s.DB, "", int32(u), false)
- c.JSON(http.StatusOK, users)
- } else if exName {
- // Get user with exact name if not search
- // Get user with name like if search
- users := sql.GetUser(s.DB, name, -1, exSearch)
- c.JSON(http.StatusOK, users)
- return
- } else {
- // Get all users
- users := sql.GetUser(s.DB, "", -1, true)
- c.JSON(http.StatusOK, users)
- }
- }
- // API:
- // GET URL: /group
- // Get all groups
- // GET URL: /group?gid=123
- // Get group with gid 123
- // GET URL: /group?name=abc
- // Get group with name abc
- // GET URL: /group?name=abc&search
- // Get groups starting with abc
- // GET URL: /group?user=abc
- // Get all groups that user with username
- // belongs to
- // GET URL: /group?uid=123
- // Get all groups that user with uid
- // belongs to.
- // Any other parameter combinations
- // returns an empty array.
- func (s swsydAPI) getGroup(c *gin.Context) {
- name, exName := c.GetQuery("name")
- gid, exGid := c.GetQuery("gid")
- _ , exSearch := c.GetQuery("search")
- uid, exUid := c.GetQuery("uid")
- user, exUser := c.GetQuery("user")
- if ! exName && ! exGid && ! exSearch && ! exUid && ! exUser {
- // Get all groups
- fmt.Println("getting all groups")
- groups := sql.GetGroup(s.DB, "", -1, true)
- c.JSON(http.StatusOK, groups)
- } else if exName && ! exGid && ! exUid && ! exUser {
- // Get group with name
- groups := sql.GetGroup(s.DB, name, -1, exSearch)
- c.JSON(http.StatusOK, groups)
- } else if exGid && ! exName && ! exSearch && ! exUid && ! exUser {
- // Get group with gid
- igid, err := strconv.Atoi(gid)
- if err != nil {
- slog.Error(fmt.Sprintf("getGroup: Failed to convert gid: %s to int. %s\n", gid, err))
- c.JSON(http.StatusOK, []sql.Group{})
- }
-
- groups := sql.GetGroup(s.DB, "", int32(igid), false)
- c.JSON(http.StatusOK, groups)
- } else if exUid && ! exGid && ! exSearch && ! exName && ! exUser {
- // Get groups for user with uid
- iuid, err := strconv.Atoi(uid)
- if err != nil {
- slog.Error(fmt.Sprintf("getGroup: Failed to convert uid: %s to int. %s\n", uid, err))
- c.JSON(http.StatusOK, []sql.Group{})
- } else {
- groups := sql.GetUserGroups(s.DB, "", int32(iuid))
- c.JSON(http.StatusOK, groups)
- }
- } else if exUser && ! exGid && ! exSearch && ! exUid && ! exName {
- // Get groups for username
- groups := sql.GetUserGroups(s.DB, user, -1)
- c.JSON(http.StatusOK, groups)
- } else {
- c.JSON(http.StatusOK, []sql.Group{})
- }
- }
- // API:
- // GET URL: /shadow
- // Get all shadow fields
- // GET URL: /shadow?user=abc
- // Get shadow fields for user
- // with username
- // GET URL: /shadow?user=abc&search
- // Get shadow fields for users
- // starting with name abc
- // GET URL: /shadow?uid=123
- // Get shadow fields for user with uid
- // Any other parameter combinations
- // returns an empty array.
- func (s swsydAPI) getShadow(c *gin.Context) {
- uid, exUid := c.GetQuery("uid")
- user, exUser := c.GetQuery("user")
- _ , exSearch := c.GetQuery("search")
- shadows := []sql.Shadow{}
-
- if !exUid && !exUser && !exSearch {
- // Get all shadow records
- shadows = sql.GetShadow(s.DB, "", -1, true)
- } else if exUid && !exUser && !exSearch {
- // Get shafow fields for user with uid
- iuid, err := strconv.Atoi(uid)
- if err != nil {
- slog.Error(fmt.Sprintf("GetShadow: Failed to convert uid string: %s to int. %s", uid, err))
- } else {
- shadows = sql.GetShadow(s.DB, "", int32(iuid), exSearch)
- }
- } else if exUser && !exUid {
- // Get shadow with username or username staring with user
- shadows = sql.GetShadow(s.DB, user, -1, exSearch)
- }
- c.JSON(http.StatusOK, shadows)
- }
- // API:
- // GET URL: /host
- // Get all hosts
- // GET URL: /host?name=abc
- // Get host with hostname name
- // GET URL: /host?name=abc&search
- // Get host weith hostname
- // starting with name abc
- // GET URL: /host?ipv4=1.2.3.4
- // Get host with IP version 4 number
- // GET URL: /host?ipv6=1234:fd2:5621:1:89::4500
- // Get host with IP version 6 number
- // Any other parameter combinations
- // returns an empty array.
- func (s swsydAPI) getHost(c *gin.Context) {
- name, exName := c.GetQuery("name")
- ipv4, exIpv4 := c.GetQuery("ipv4")
- ipv6, exIpv6 := c.GetQuery("ipv6")
- _ , exSearch := c.GetQuery("search")
- hosts := []sql.Host{}
- if !exName && !exSearch && !exIpv4 && !exIpv6 {
- // Get all hosts
- hosts = sql.GetHost(s.DB, "", "", "", true)
- } else if exName && !exSearch && !exIpv4 && !exIpv6{
- // Get host with name
- hosts = sql.GetHost(s.DB, name, "", "", false)
- } else if exName && exSearch && !exIpv4 && !exIpv6 {
- // Get host starting with name
- hosts = sql.GetHost(s.DB, name, "", "", true)
- } else if exIpv4 && !exIpv6 && !exName && !exSearch {
- // Get host with IPv4
- hosts = sql.GetHost(s.DB, "", ipv4, "", false)
- } else if exIpv6 && !exIpv4 && !exName && !exSearch {
- // Get host with IPv6
- hosts = sql.GetHost(s.DB, "", "", ipv6, false)
- }
- c.JSON(http.StatusOK, hosts)
- }
- // Add user
- // API:
- // POST URL: /user
- // With the following form fields:
- // username: Required string
- // uid: optional positive integer
- // gid: optional positive integer
- // gecos: optional string
- // home: optional string (path)
- // shell: optional string (path to shell)
- func (s swsydAPI) postUser(c *gin.Context) {
- response := PostResponse{}
- u := sql.Password{}
-
- err := c.Bind(&u)
- if err != nil {
- slog.Error(fmt.Sprintf("postUser: Failed to parse supplied form to user: %s", err))
- response.Message = fmt.Sprintf("Failed to parse supplied form: %s", err)
- c.JSON(http.StatusBadRequest, response)
- } else if len(u.Username) == 0 {
- response.Message = "username is required"
- c.JSON(http.StatusBadRequest, response)
- }
- if len(u.Home) == 0 {
- u.Home = filepath.Join(s.conf.DefaultHome, u.Username)
- }
- if len(u.Shell) == 0 {
- u.Shell = s.conf.DefaultShell
- }
- if len(u.Password) != 0 {
- u.Password = ""
- }
- if err := sql.InsertUser(s.DB, s.conf, u); err != nil {
- response.Message = fmt.Sprintf("%s",err)
- c.JSON(http.StatusInternalServerError, response)
- }
- response.Message = fmt.Sprintf("User: %s successfully created", u.Username)
- c.JSON(http.StatusCreated, response)
- }
- // Add group
- // API:
- // POST URL: /group
- // With the following form fields:
- // groupname: Required string
- // gid: optional positive integer
- // password: clear text string
- func (s swsydAPI) postGroup(c *gin.Context) {
- response := PostResponse{}
- g := sql.Group{}
- err := c.Bind(&g)
- if err != nil {
- slog.Error(fmt.Sprintf("postGroup: Failed to parse group from form: %s", err))
- response.Message = fmt.Sprintf("Failed to parse form: %s", err)
- c.JSON(http.StatusBadRequest, response)
- return
- } else if len(g.Groupname) == 0 {
- response.Message = "groupname is required"
- c.JSON(http.StatusBadRequest, response)
- return
- }
- if len(g.GroupPassword) > 0 {
- hash, err := passwordSha512(g.GroupPassword)
- if err != nil {
- slog.Error(fmt.Sprintf("%s", err))
- response.Message = "Failed to hash password"
- c.JSON(http.StatusInternalServerError, response)
- return
- } else {
- g.GroupPassword = hash
- }
- }
- err = sql.InsertGroup(s.DB, s.conf, g)
- if err != nil {
- slog.Error("Failed to save group: %s, to database: %s", g, err)
- response.Message = fmt.Sprintf("%s", err)
- c.JSON(http.StatusInternalServerError, response)
- return
- }
- response.Message = "Group successfully added."
- c.JSON(http.StatusOK, response)
- }
- // Add swadow
- // API:
- // POST URL: /shadow
- // With the following form fields:
- // username: Required string if not UID
- // uid: Required integer if not username
- // password: clear text string
- // lastchange: optional integer
- // minpasswordage: optional integer
- // maxpasswordage: optional integer
- // warningperiod: optional integer
- // inactiveperiod: optional integer
- // expiredate: string formated as date
- func (s swsydAPI) postShadow(c *gin.Context) {
- response := PostResponse{}
- sh := sql.Shadow{}
- uid := -1
- uidStr := c.PostForm("uid")
-
- err := c.Bind(&sh)
- if err != nil {
- slog.Error(fmt.Sprintf("postShadow: Failed to parse shadow from form: %s", err))
- response.Message = fmt.Sprintf("Failed to parse form: %s", err)
- c.JSON(http.StatusBadRequest, response)
- return
- } else if len(sh.Username) == 0 && len(uidStr) == 0 {
- response.Message = "username or uid is required"
- c.JSON(http.StatusBadRequest, response)
- return
- }
- if len(uidStr) > 0 {
- uid, err = strconv.Atoi(uidStr)
- if err != nil {
- slog.Error(fmt.Sprintf("postShadow: Failed to convert uid: %s to int: %s", uidStr, err))
- response.Message = fmt.Sprintf("Failed to convert uid: %s to integer.", uidStr)
- c.JSON(http.StatusBadRequest, response)
- return
- }
- }
-
- if len(sh.Password) > 0 {
- hash, err := passwordSha512(sh.Password)
- if err != nil {
- slog.Error(fmt.Sprintf("postShadow: %s", err))
- response.Message = "Failed to hash password"
- c.JSON(http.StatusInternalServerError, response)
- return
- } else {
- sh.Password = hash
- }
- }
- err = sql.InsertShadow(s.DB, s.conf, sh, int32(uid))
- if err != nil {
- slog.Error("Failed to save shadow: %s, to database: %s", sh, err)
- response.Message = fmt.Sprintf("%s", err)
- c.JSON(http.StatusInternalServerError, response)
- return
- }
- response.Message = "Password successfully added."
- c.JSON(http.StatusOK, response)
- }
- // Add host
- // API:
- // POST URL: /host
- // With the following form fields:
- // ipv4: IPv4 address, eg 192.168.1.16
- // ipv6: IPv6 address
- // hostnames: string with space separated hostnames, required
- func (s swsydAPI) postHost(c *gin.Context) {
- response := PostResponse{}
- h := sql.Host{}
-
- err := c.Bind(&h)
- if err != nil {
- slog.Error(fmt.Sprintf("postHost: Failed to parse host from form: %s", err))
- response.Message = fmt.Sprintf("Failed to parse form: %s", err)
- c.JSON(http.StatusBadRequest, response)
- return
- }
- if len(h.Hostnames) == 0 {
- slog.Error("postHost: hostnames is required")
- response.Message = "Failed to add host, hostnames is required."
- c.JSON(http.StatusBadRequest, response)
- return
- }
- if len(h.IPv4) > 0 {
- ip4 := net.ParseIP(h.IPv4)
- if ip4 == nil {
- slog.Error(fmt.Sprintf("postHost: IPv4: %s, is not a valid IP address", h.IPv4))
- response.Message = fmt.Sprintf("Failed to add host, IPv4: %s is not a valid IP address.", h.IPv4)
- c.JSON(http.StatusBadRequest, response)
- return
- }
- }
- if len(h.IPv6) > 0 {
- ipv6 := net.ParseIP(h.IPv6)
- if ipv6 == nil {
- slog.Error(fmt.Sprintf("postHost: IPv6: %s, is not a valid IP address", h.IPv6))
- response.Message = fmt.Sprintf("Failed to add host, IPv6; %s is not a valid IP address", h.IPv6)
- c.JSON(http.StatusBadRequest, response)
- return
- }
- }
- h.Hostnames = strings.TrimSpace(h.Hostnames)
- regNames := regexp.MustCompile("^[a-zA-Z0-9][a-zA-Z0-9.-]*[a-zA-Z0-9]$")
- // TODO
- // clean up muliple hosts, remove multiple spaces between names
- hostnames := ""
- for _, n := range strings.Fields(h.Hostnames) {
- if ! regNames.MatchString(n) {
- slog.Error("postHost: Hostnasme: %s is an invalid hostname", n)
- 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)
- c.JSON(http.StatusBadRequest, response)
- return
- }
- if len(hostnames) == 0 {
- hostnames = n
- } else {
- hostnames = fmt.Sprintf("%s %s", hostnames, n)
- }
- }
- h.Hostnames = hostnames
- err = sql.InsertHost(s.DB, s.conf, h)
- if err != nil {
- slog.Error(fmt.Sprintf("postHost: Failed to save host: %+v to database: %s", h, err))
- response.Message = fmt.Sprintf("%s", err)
- c.JSON(http.StatusInternalServerError, response)
- return
- }
- response.Message = "Host succesfully added."
- c.JSON(http.StatusOK, response)
- }
- // Hash password according to
- // unix shadow file
- // sha512
- func passwordSha512(pwd string) (string, error) {
- const saltSize = 16
- var salt = make([]byte, saltSize)
- _, err := rand.Read(salt[:])
- if err != nil {
- return "", fmt.Errorf("Failed to generate salt for sha512 hash password: %s", err)
- }
- salt = []byte(fmt.Sprintf("$6$%x", salt))
- c := sha512_crypt.New()
- hash, err := c.Generate([]byte(pwd), salt)
- if err != nil {
- return "", fmt.Errorf("Failed to generate password hash sha512")
- }
- return string(hash), nil
- }
- // Prints version to stdout
- func printVersion() {
- fmt.Println("swsyd", VERSION)
- fmt.Printf("Copyright (C) 2024 %s.\n", AUTHORS)
- fmt.Println("License GPLv3+: GNU GPL version 3 or later <https://gnu.org/licenses/gpl.html>")
- fmt.Println("This is free software: you are free to change and redistribute it.")
- fmt.Println("There is NO WARRANTY, to the extent permitted by law.")
- fmt.Println("")
- fmt.Printf("Written by %s.\n", AUTHORS)
- }
- // Prints help text to stdout
- func printHelp() {
- fmt.Println("Usage: swsyd [OPTION]...")
- fmt.Println("swsy daemon, make nsswitch easy")
- fmt.Println("")
- fmt.Println("Without any arguments swsyd will start in daemon mode.")
- fmt.Println("Deamon responds both to nsswitch clients and admin")
- fmt.Println("client that manages users, groups and hosts.")
- fmt.Println("")
- fmt.Println("Options:")
- fmt.Println(" -h, --help Prints this helptext and exit.")
- fmt.Println(" -V, --version Prints version information and exit.")
- }
- // Initialize log
- func initLog(c common.Config) {
- logHandle, err := os.OpenFile(c.Logfile,
- os.O_WRONLY|os.O_APPEND|os.O_CREATE, 0644)
- if err != nil {
- fmt.Fprintf(os.Stderr, "Failed to open logfile: %s, printing log to Stdout.\n%s\n",
- c.Logfile, err)
- } else {
- log.SetOutput(logHandle)
- }
- slog.SetLogLoggerLevel(c.Loglevel)
- }
- // Program start
- func main() {
- if len(os.Args) >= 3 {
- fmt.Fprintf(os.Stderr, "Wrong arguments.\nTry -h or --help for help.\n")
- os.Exit(1)
- } else if len(os.Args) == 2 {
- if os.Args[1] == "--version" || os.Args[1] == "-V" {
- printVersion()
- os.Exit(0)
- } else if os.Args[1] == "--help" || os.Args[1] == "-h" {
- printHelp()
- os.Exit(0)
- } else {
- fmt.Fprintf(os.Stderr, "Wrong arguments.\nTry -h or --help for help.\n")
- os.Exit(1)
- }
- }
- conf := common.GetConfig()
- initLog(conf)
- slog.Info("Starting swsyd")
- swsyd := swsydAPI{}
- db, err := sql.InitDb(conf)
- if err != nil {
- slog.Error(fmt.Sprintf("Failed to initialize database: %s", conf.Dbfile))
- slog.Error(fmt.Sprintf("%s", err))
- slog.Error("Terminating swsyd")
- os.Exit(3)
- }
- swsyd.conf = conf
- swsyd.DB = db
- defer func() {
- if db, err := swsyd.DB.DB(); err != nil {
- slog.Error("Failed to close database connection:")
- slog.Error(fmt.Sprintf("%s", err))
- } else {
- db.Close()
- }
- }()
-
- g := gin.Default()
- g.GET("/user", swsyd.getUser)
- g.GET("/group", swsyd.getGroup)
- g.GET("/shadow", swsyd.getShadow)
- g.GET("/host",swsyd.getHost)
- g.POST("/user", swsyd.postUser)
- g.POST("/group", swsyd.postGroup)
- g.POST("/shadow", swsyd.postShadow)
- g.POST("/host", swsyd.postHost)
- g.Run(fmt.Sprintf(":%d", conf.Port))
- }
|