Linux NSS Module Development for Keycloak OIDC

What is NSS ?

  • NSS or Name Service Switch is a feature present in UNIX and LINUX systems. NSS  connects the system with different variety of sources of common configuration and name resolution mechanisms.
  • Some of the common sources are /etc/passwd for user details, /etc/group for group details, /etc/shadow for password , /etc/hosts for DNS etc.
  • NSS utility is used internally by any other applications like ssh. If ssh daemon want to check for a user, it will use nss module to do the lookup for the user.
  • nsswitch.conf is the configuration file, in which we define different databases or configuration file, which a system application can query to.
  • Below snippet shows trimmed output of nsswitch.conf.
passwd:     files ldap
shadow:     files
group:      files ldap

hosts:      dns nis files

ethers:     files nis
netmasks:   files nis
  • It can be seen that the parameter passwd is followed by a colon (:) and the available services, which is files and ldap
  • Files here mentioned for passwd, will be /etc/passwd
  • Similarly files mentioned for group will be /etc/group
  • shared object files for these will be present under /etc/lib64/ (path may be different on different distros) with the name libnss_files.so.2 and libnss_ldap.so.2

In this article, we will see how to write/create a shared object or binary , which is capable of communicating with keycloak and to do user lookup or group lookup for the system

Shared object Naming Scheme

  • It is important to follow the nomenclature defined for the nss modules
  • The binary name should start with libnss_ and should service name
  • In our case service name is ldh, so the shared object binary name will be libnss_ldh.so.2

Configuration changes

  • The shared object binary should be placed in the path /lib/usr64/ (path may be different in different linux distros, refer official linux distros documentation)
  • The nss configuration file, /etc/nsswitch.conf  need to be updated with the service name ldh.
[root@LDH]# cat /etc/nsswitch.conf
passwd: files [SUCCESS=return] sss ldh
shadow: files [SUCCESS=return] sss
group: files [SUCCESS=return] sss ldh

User look up flows

Below diagram shows the nss lookup for passwd and group database

Linux NSS Module Development for Keycloak OIDC

  • When a application needs user or group details , it will traverse check in the services mentioned in the nsswitch.conf.
  • When it reaches the service ldh, libnss_ldh.so.2 will query to keycloak server, and store the contents in the SQL DB
  • The query will include users and group details along with their attributes
  • Once SQL DB is populated with the details, libnss_ldh.so.2 will query the users or groups from the DB and will share the output to the application who invoked the nss

Setup Details

Pre-Requisites

  • These below pre-requisites should be followed, as our sample code is dependent on it. Skipping any of these steps may impact the operation of keycloak nss module
  • keycloak should be installed and running
  • A new realm apart from master realm need to be created ( for this article, i have created a realm called uma)
  • A user (casy) with credentials should be created in the newly created realm
  • While setting the credentials for the new users, make sure the flag Temporary is turned off
  • For the sake of this article, i will be using http rather than https, you can  enable it going to  realm (uma)→  Realm Settings → Login → Require SSL (select None from drop down)
  • User (casy) or users have to created in the realm uma with the following attributes
      • home dir : /home/casy
      • shell : /bin/bash
      • uid: 8934
  • Group (casy) with the same username should be created with below attributes
      • gid: 9823
  • User (casy) need to be added to the group (casy)  (primary group)
  • Any other group with attribute gid can be created and the created  user can be assigned to this group (secondary group) [Optional]

Code Walkthrough

  • We are using go-libnss module which is a wrapper around unix libnss library
  • We will be having two scripts, one is having the the logic to query to keycloak and populated DB. Other script contains the methods which utilizes the created db
      • db.go
      • nss.go

db.go

  • In this script, we will be defining and init function
  • The init function will contain logic to fetch details from keycloak to sql DB
  • From the SQL DB we will querying and returning data's in a struct format which is understandable by methods implemented in the go-libnss module
package main

import (
	"crypto/tls"
	"database/sql"
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"net/url"
	"os"
	"strconv"
	"strings"

	. "github.com/protosam/go-libnss/structs"

	_ "github.com/mattn/go-sqlite3" // Import go-sqlite3 library
)

const (
	TOKEN_NOT_FOUND int = iota
)

type GroupMemberinfo struct {
	Username string `usernam:"type,omitempty"`
	// Attributes Attribute `attributes:"type,omitempty"`
}
type UserGroupInfo struct {
	Id   string `id:"type,omitempty"`
	Name string `name:"type,omitempty"`
	Path string `path:"type,omitempty"`
	// Attributes Attribute `attributes:"type,omitempty"`
}
type GroupIdInfo struct {
	// Id         string         `id:"type,omitempty"`
	// Name       string         `name:"type,omitempty"`
	// Path       string         `path:"type,omitempty"`
	Attributes GroupAttribute `attributes:"type,omitempty"`
}

type GroupAttribute struct {
	Gid []string `gid:"type,omitempty"`
	// Gid []string `json:"gid,omitempty"`
}

type UserInfo struct {
	Id         string    `id:"type,omitempty"`
	Username   string    `usernam:"type,omitempty"`
	Attributes Attribute `attributes:"type,omitempty"`
}
type Attribute struct {
	Uids     []string `json:"uid,omitempty"`
	Shells   []string `json:"shell,omitempty"`
	Homedirs []string `json:"homedir,omitempty"`
}

var Gid string
var Gid_uint uint
var Uid_uint uint
var Groupmember string

var dbtest_passwd []Passwd
var dbtest_group []Group
var dbtest_shadow []Shadow

func init() {

	// var Username string
	tr := &http.Transport{
		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
	}
	token_client := &http.Client{Transport: tr}
	openIDURL := "http://10.39.251.173:8080/realms/master/protocol/openid-connect/token"
	// logrus.Info("OIDC Endpoint is" + openIDURL)
	formData := url.Values{
		"client_id":     {"admin-cli"},
		"grant_type":    {"password"},
		"client_secret": {""},
		"username":      {"admin"},
		"password":      {"admin"},
	}
	resp, err := token_client.PostForm(openIDURL, formData)
	if err != nil {
		fmt.Println(err.Error())

		// logrus.Warn(err.Error())
	}
	defer resp.Body.Close()
	var result map[string]interface{}
	json.NewDecoder(resp.Body).Decode(&result)
	// logrus.Info(result)
	token := result["access_token"].(string)

	// client := gocloak.NewClient("http://10.39.251.173:8080", gocloak.SetAuthAdminRealms("admin/realms"), gocloak.SetAuthRealms("realms"))
	// ctx := context.Background()
	// token, err := client.LoginClient(ctx, "nss-client", "abcd", "master")
	if err != nil {
		panic("Login failed:" + err.Error())
	}

	tr = &http.Transport{
		TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
	}
	user_client := &http.Client{Transport: tr}
	group_client := &http.Client{Transport: tr}
	gid_client := &http.Client{Transport: tr}

	req, err := http.NewRequest("GET", "http://10.39.251.173:8080/admin/realms/uma/users?briefRepresentation=false", nil)
	if err != nil {
		// handle err
	}
	req.Header.Set("Authorization", os.ExpandEnv("bearer "+token))
	req.Header.Set("Content-Type", "application/json")

	resp, err = user_client.Do(req)
	if err != nil {
		// handle err
	}
	defer resp.Body.Close()
	db_creation()

	var uInfo []UserInfo

	err = json.NewDecoder(resp.Body).Decode(&uInfo)
	for _, s := range uInfo {
		// fmt.Println(s)
		// Group query start#######################################################################################
		req, err := http.NewRequest("GET", "http://10.39.251.173:8080/admin/realms/uma/groups", nil)
		if err != nil {
			// handle err
		}
		req.Header.Set("Authorization", os.ExpandEnv("bearer "+token))
		req.Header.Set("Content-Type", "application/json")

		resp_group, err := group_client.Do(req)
		if err != nil {
			// handle err
		}
		defer resp_group.Body.Close()
		var gInfo []UserGroupInfo
		err = json.NewDecoder(resp_group.Body).Decode(&gInfo)
		if err != nil {
			log.Fatal(err.Error())
		}
		// fmt.Println(gInfo)
		for _, gr := range gInfo {

			if gr.Name == s.Username {
				// fmt.Println("########################")
				req, err := http.NewRequest("GET", "http://10.39.251.173:8080/admin/realms/uma/groups/"+gr.Id, nil)
				if err != nil {
					log.Fatal(err.Error())

					// handle err
				}
				req.Header.Set("Authorization", os.ExpandEnv("bearer "+token))
				req.Header.Set("Content-Type", "application/json")

				resp_groupid, err := gid_client.Do(req)
				if err != nil {
					// handle err
				}

				defer resp_groupid.Body.Close()

				var gIdInfo GroupIdInfo

				err = json.NewDecoder(resp_groupid.Body).Decode(&gIdInfo)

				if err != nil {
					log.Fatal(err.Error())
				}
				// fmt.Println("s.Username")

				Gid = gIdInfo.Attributes.Gid[0]
				u64, err := strconv.ParseUint(Gid, 10, 32)
				if err != nil {
					fmt.Println(err)
				}
				Gid_uint = uint(u64)
				// fmt.Println(wd)

			}
		}

		// Group query end ###############################################################################
		inux_db, err := sql.Open("sqlite3", "/root/sqlite-database.db")
		if err != nil {
			log.Fatal(err.Error())
		}
		defer inux_db.Close()

		u64, err := strconv.ParseUint(s.Attributes.Uids[0], 10, 32)
		if err != nil {
			fmt.Println(err)
		}
		Uid_uint = uint(u64)

		insertUser(inux_db, s.Username, Uid_uint, Gid_uint, s.Attributes.Homedirs[0], s.Attributes.Shells[0])

	}

	// # started writing into groups table

	req4, err := http.NewRequest("GET", "http://10.39.251.173:8080/admin/realms/uma/groups", nil)
	if err != nil {
		// handle err
	}
	req4.Header.Set("Authorization", os.ExpandEnv("bearer "+token))
	req4.Header.Set("Content-Type", "application/json")

	resp_group, err := group_client.Do(req4)
	if err != nil {
		// handle err
	}

	defer resp_group.Body.Close()
	var gInfo []UserGroupInfo
	err = json.NewDecoder(resp_group.Body).Decode(&gInfo)
	if err != nil {
		log.Fatal(err.Error())
	}
	inux_db, err := sql.Open("sqlite3", "/root/sqlite-database.db")
	if err != nil {
		log.Fatal(err.Error())
	}

	defer inux_db.Close()
	for _, grp := range gInfo {
		req, err := http.NewRequest("GET", "http://10.39.251.173:8080/admin/realms/uma/groups/"+grp.Id, nil)
		if err != nil {
			log.Fatal(err.Error())

			// handle err
		}
		req.Header.Set("Authorization", os.ExpandEnv("bearer "+token))
		req.Header.Set("Content-Type", "application/json")

		resp_groupid, err := gid_client.Do(req)
		if err != nil {
			// handle err
		}

		defer resp_groupid.Body.Close()

		var gIdInfo GroupIdInfo

		err = json.NewDecoder(resp_groupid.Body).Decode(&gIdInfo)

		if err != nil {
			log.Fatal(err.Error())
		}
		// fmt.Println("s.Username")

		// fmt.Println("s.Username")

		Gid = gIdInfo.Attributes.Gid[0]
		u64, err := strconv.ParseUint(Gid, 10, 32)
		if err != nil {
			fmt.Println(err)
		}

		Gid_uint = uint(u64)
		// #########$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
		req5, err := http.NewRequest("GET", "http://10.39.251.173:8080/admin/realms/uma/groups/"+grp.Id+"/members", nil)
		// fmt.Println(grp.Id)
		if err != nil {
			// handle err
		}
		req5.Header.Set("Authorization", os.ExpandEnv("bearer "+token))
		// fmt.rintln(token.AccessToken)
		req5.Header.Set("Content-Type", "application/json")

		resp_group, err := group_client.Do(req5)
		if err != nil {
			// handle err
		}

		defer resp_group.Body.Close()
		// b, err := io.ReadAll(resp_group.Body)
		// // b, err := ioutil.ReadAll(resp.Body)  Go.1.15 and earlier
		// if err != nil {
		// 	log.Fatalln(err)
		// }

		// fmt.Println(string(b))
		var gMInfo []GroupMemberinfo
		err = json.NewDecoder(resp_group.Body).Decode(&gMInfo)
		if err != nil {
			log.Fatal(err.Error())
		}

		defer inux_db.Close()
		for _, grpq := range gMInfo {
			// fmt.Println(grpq.Username)
			Groupmember += grpq.Username + ","
		}
		// fmt.Println(groupmember)
		inux_db, err := sql.Open("sqlite3", "/root/sqlite-database.db")
		if err != nil {
			log.Fatal(err.Error())
		}
		// fmt.Println(Groupmember)
		insertGroup(inux_db, grp.Name, Gid_uint, Groupmember)
		Groupmember = ""

	}
	db, err := sql.Open("sqlite3", "/root/sqlite-database.db")
	if err != nil {
		log.Fatal(err.Error())
	}
	defer db.Close()

	row, err := db.Query("SELECT * FROM users ")
	if err != nil {
		log.Fatal(err)
	}
	defer row.Close()
	for row.Next() { // Iterate and fetch the records from result cursor
		var Username string
		var UID uint
		var GID uint
		var Dir string
		var Shell string
		row.Scan(&Username, &UID, &GID, &Dir, &Shell)
		// log.Println("Student: ", Username, " ", UID, " ", GID, " ", Dir, " ", Shell)
		dbtest_passwd = append(dbtest_passwd,
			Passwd{
				Username: Username,
				Password: "x",
				UID:      UID,
				GID:      GID,
				Gecos:    "Test user 1",
				Dir:      Dir,
				Shell:    Shell,
			},
		)
		// fmt.Printf("%+v \n", dbtest_passwd)
	}

	row, err = db.Query("SELECT * FROM groups ")
	if err != nil {
		log.Fatal(err)
	}
	defer row.Close()
	for row.Next() { // Iterate and fetch the records from result cursor
		var Groupname string
		var GID uint
		var Members string
		row.Scan(&Groupname, &GID, &Members)
		// fmt.Println(Members)
		split := strings.Split(Members, ",")
		split = delete_empty(split)
		// fmt.Println(split)
		Slack := []string{}
		for i := 0; i < len(split); i++ {
			// ustString := strings.Join(split[i], " ")
			// fmt.Println(len(split))
			// fmt.Println(split[i])
			Slack = append(Slack, split[i])
		}

		dbtest_group = append(dbtest_group,
			Group{
				Groupname: Groupname,
				Password:  "x",
				GID:       GID,
				Members:   Slack,
			},
		)
	}
	// fmt.Printf("%+v \n", dbtest_group)

	dbtest_shadow = append(dbtest_shadow,
		Shadow{
			Username:        "Srtyu",
			Password:        "$6$yZcX.DOY$7bgsJhILMYl3DfMZsYUwoObbVt5Sj9FuujuhVn05Vg9hk.2AXLNy6o1DcPNq0SIyaRZ5YBZer2rYaycuh3qtg1", // Password is "password"
			LastChange:      17920,
			MinChange:       0,
			MaxChange:       99999,
			PasswordWarn:    7,
			InactiveLockout: -1,
			ExpirationDate:  -1,
			Reserved:        -1,
		},
		Shadow{
			Username:        "web",
			Password:        "$6$yZcX.DOY$7bgsJhILMYl3DfMZsYUwoObbVt5Sj9FuujuhVn05Vg9hk.2AXLNy6o1DcPNq0SIyaRZ5YBZer2rYaycuh3qtg1", // Password is "password"
			LastChange:      17920,
			MinChange:       0,
			MaxChange:       99999,
			PasswordWarn:    7,
			InactiveLockout: 0,
			ExpirationDate:  0,
			Reserved:        -1,
		},
	)

	// fmt.Printf("%+v \n", dbtest_passwd)
	// fmt.Printf("%+v \n", dbtest_group)
	// fmt.Printf("%+v \n", dbtest_shadow)

}

func delete_empty(s []string) []string {
	var r []string
	for _, str := range s {
		if str != "" {
			r = append(r, str)
		}
	}
	return r
}

func db_creation() {
	os.Remove("/root/sqlite-database.db")
	// fmt.Println("Creating sqlite-database.db...")
	file, err := os.Create("sqlite-database.db") // Create SQLite file
	if err != nil {
		log.Fatal(err.Error())
	}
	file.Close()
	// fmt.Println("sqlite-database.db created")
	linux_db, err := sql.Open("sqlite3", "/root/sqlite-database.db")
	if err != nil {
		log.Fatal(err.Error())
	}
	defer linux_db.Close()
	linux_db_table := `CREATE TABLE users ( Username string PRIMARY KEY,UID INT NOT NULL, GID INT NOT NULL,Dir string NOT NULL,Shell string NOT NULL);` // SQL Statement for Create Table

	// log.Println("Create user table...")
	statement, err := linux_db.Prepare(linux_db_table) // Prepare SQL Statement
	if err != nil {
		log.Fatal(err.Error())
	}
	statement.Exec() // Execute SQL Statements
	// log.Println("user table created")

	linux_db_group_table := `CREATE TABLE groups ( Groupname string PRIMARY KEY,GID INT NOT NULL,Members string NOT NULL);` // SQL Statement for Create Table

	// log.Println("Create group table...")
	statement, err = linux_db.Prepare(linux_db_group_table) // Prepare SQL Statement
	if err != nil {
		log.Fatal(err.Error())
	}
	statement.Exec() // Execute SQL Statements
	// log.Println("group table created")

}

func insertUser(db *sql.DB, username string, uid uint, gid uint, dir string, shell string) {
	// log.Println("Inserting user record ...")
	insertuser := `INSERT INTO users(username, uid, gid,dir,shell) VALUES (?,?,?, ?,?)`
	statement, err := db.Prepare(insertuser) // Prepare statement.
	// This is good to avoid SQL injections
	if err != nil {
		log.Fatalln(err.Error())
	}
	_, err = statement.Exec(username, uid, gid, dir, shell)
	if err != nil {
		log.Fatalln(err.Error())
	}
}
func insertGroup(db *sql.DB, groupname string, gid uint, members string) {

	// log.Println("Inserting user record ...")
	insertgroup := `INSERT INTO groups(groupname,gid,members) VALUES (?,?,?)`

	statement, err := db.Prepare(insertgroup) // Prepare statement.
	// This is good to avoid SQL injections
	if err != nil {
		log.Fatalln(err.Error())
	}

	_, err = statement.Exec(groupname, gid, members)
	if err != nil {
		log.Fatalln(err.Error())
	}
}
  • Below code snippet shows the logic to get authentication token from keycloak.
  • admin-cli client with grant_type password is used
    token_client := &http.Client{Transport: tr}
    openIDURL := "http://10.39.251.173:8080/realms/master/protocol/openid-connect/token"
    // logrus.Info("OIDC Endpoint is" + openIDURL)
    formData := url.Values{
        "client_id":     {"admin-cli"},
        "grant_type":    {"password"},
       "client_secret": {""},
        "username":      {"admin"},
        "password":      {"admin"},
    }
    resp, err := token_client.PostForm(openIDURL, formData)
    if err != nil {
        fmt.Println(err.Error())
        // logrus.Warn(err.Error())
    }
    defer resp.Body.Close()
    var result map[string]interface{}
    json.NewDecoder(resp.Body).Decode(&result)
    // logrus.Info(result)
    token := result["access_token"].(string)
  • Below code snippet shows the logic to fetch the users from the realm along with their attributes and assigning to a struct
  • The content of this struct is later used for populating the db
    req, err := http.NewRequest("GET", "http://10.39.251.173:8080/admin/realms/uma/users?briefRepresentation=false", nil)
    if err != nil {
        // handle err
    }
    req.Header.Set("Authorization", os.ExpandEnv("bearer "+token))
    req.Header.Set("Content-Type", "application/json")
    resp, err = user_client.Do(req)
    if err != nil {
        // handle err
    }
    defer resp.Body.Close()
    db_creation()
    var uInfo []UserInfo
    err = json.NewDecoder(resp.Body).Decode(&uInfo)

 

  • Below code snippet shows the logic implemented to get the primary group's gid
  • First the groups of the realm is queried from keycloak, which will provide the group id ( not gid)
  • Querying the groups from the realm, will not give the group's attributes, for that we need to query the groups individually
  • Secondly the group attributes is queried using the group id (not gid) to fetch the gid (which is given as attributes)
	req, err := http.NewRequest("GET", "http://10.39.251.173:8080/admin/realms/uma/groups", nil)
		if err != nil {
			// handle err
		}
		req.Header.Set("Authorization", os.ExpandEnv("bearer "+token))
		req.Header.Set("Content-Type", "application/json")

		resp_group, err := group_client.Do(req)
		if err != nil {
			// handle err
		}
		defer resp_group.Body.Close()
		var gInfo []UserGroupInfo
		err = json.NewDecoder(resp_group.Body).Decode(&gInfo)
		if err != nil {
			log.Fatal(err.Error())
		}
		// fmt.Println(gInfo)
		for _, gr := range gInfo {

			if gr.Name == s.Username {
				// fmt.Println("########################")
				req, err := http.NewRequest("GET", "http://10.39.251.173:8080/admin/realms/uma/groups/"+gr.Id, nil)
				if err != nil {
					log.Fatal(err.Error())

					// handle err
				}
				req.Header.Set("Authorization", os.ExpandEnv("bearer "+token))
				req.Header.Set("Content-Type", "application/json")

				resp_groupid, err := gid_client.Do(req)
				if err != nil {
					// handle err
				}

				defer resp_groupid.Body.Close()

				var gIdInfo GroupIdInfo

				err = json.NewDecoder(resp_groupid.Body).Decode(&gIdInfo)

				if err != nil {
					log.Fatal(err.Error())
				}
				// fmt.Println("s.Username")

				Gid = gIdInfo.Attributes.Gid[0]
				u64, err := strconv.ParseUint(Gid, 10, 32)
				if err != nil {
					fmt.Println(err)
				}
				Gid_uint = uint(u64)
				// fmt.Println(wd)

			}
  • Below code snippet shows, filling the SQL DB with the details recieved from the keycloak
  • Function insertUser is having the logic to write to the SQL DB
        inux_db, err := sql.Open("sqlite3", "/root/sqlite-database.db")
        if err != nil {
            log.Fatal(err.Error())
        }
        defer inux_db.Close()
        u64, err := strconv.ParseUint(s.Attributes.Uids[0], 10, 32)
        if err != nil {
            fmt.Println(err)
        }
        Uid_uint = uint(u64)
        insertUser(inux_db, s.Username, Uid_uint, Gid_uint, s.Attributes.Homedirs[0], s.Attributes.Shells[0])
  • Below code snippet shows, fetching group details from keycloak
  • Initially group names from the realms is queried
  • Then group attributes (gid) is queried from keycloak
  • Lastly group members are queried and it is written to SDL DB
// # started writing into groups table

	req4, err := http.NewRequest("GET", "http://10.39.251.173:8080/admin/realms/uma/groups", nil)
	if err != nil {
		// handle err
	}
	req4.Header.Set("Authorization", os.ExpandEnv("bearer "+token))
	req4.Header.Set("Content-Type", "application/json")

	resp_group, err := group_client.Do(req4)
	if err != nil {
		// handle err
	}

	defer resp_group.Body.Close()
	var gInfo []UserGroupInfo
	err = json.NewDecoder(resp_group.Body).Decode(&gInfo)
	if err != nil {
		log.Fatal(err.Error())
	}
	inux_db, err := sql.Open("sqlite3", "/root/sqlite-database.db")
	if err != nil {
		log.Fatal(err.Error())
	}

	defer inux_db.Close()
	for _, grp := range gInfo {
		req, err := http.NewRequest("GET", "http://10.39.251.173:8080/admin/realms/uma/groups/"+grp.Id, nil)
		if err != nil {
			log.Fatal(err.Error())

			// handle err
		}
		req.Header.Set("Authorization", os.ExpandEnv("bearer "+token))
		req.Header.Set("Content-Type", "application/json")

		resp_groupid, err := gid_client.Do(req)
		if err != nil {
			// handle err
		}

		defer resp_groupid.Body.Close()

		var gIdInfo GroupIdInfo

		err = json.NewDecoder(resp_groupid.Body).Decode(&gIdInfo)

		if err != nil {
			log.Fatal(err.Error())
		}
		// fmt.Println("s.Username")

		// fmt.Println("s.Username")

		Gid = gIdInfo.Attributes.Gid[0]
		u64, err := strconv.ParseUint(Gid, 10, 32)
		if err != nil {
			fmt.Println(err)
		}

		Gid_uint = uint(u64)
		// #########$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$$
		req5, err := http.NewRequest("GET", "http://10.39.251.173:8080/admin/realms/uma/groups/"+grp.Id+"/members", nil)
		// fmt.Println(grp.Id)
		if err != nil {
			// handle err
		}
		req5.Header.Set("Authorization", os.ExpandEnv("bearer "+token))
		// fmt.rintln(token.AccessToken)
		req5.Header.Set("Content-Type", "application/json")

		resp_group, err := group_client.Do(req5)
		if err != nil {
			// handle err
		}

		defer resp_group.Body.Close()
		// b, err := io.ReadAll(resp_group.Body)
		// // b, err := ioutil.ReadAll(resp.Body)  Go.1.15 and earlier
		// if err != nil {
		// 	log.Fatalln(err)
		// }

		// fmt.Println(string(b))
		var gMInfo []GroupMemberinfo
		err = json.NewDecoder(resp_group.Body).Decode(&gMInfo)
		if err != nil {
			log.Fatal(err.Error())
		}

		defer inux_db.Close()
		for _, grpq := range gMInfo {
			// fmt.Println(grpq.Username)
			Groupmember += grpq.Username + ","
		}
		// fmt.Println(groupmember)
		inux_db, err := sql.Open("sqlite3", "/root/sqlite-database.db")
		if err != nil {
			log.Fatal(err.Error())
		}
		// fmt.Println(Groupmember)
		insertGroup(inux_db, grp.Name, Gid_uint, Groupmember)
		Groupmember = ""

	}
  • Below code snippet shows, populating the passwd and group struct from the details from the SQL DB
  • Shadow file with dummy details is also provided [This can be removed, provided the shadow methods are removed from the nss.go ]
row, err := db.Query("SELECT * FROM users ")
	if err != nil {
		log.Fatal(err)
	}
	defer row.Close()
	for row.Next() { // Iterate and fetch the records from result cursor
		var Username string
		var UID uint
		var GID uint
		var Dir string
		var Shell string
		row.Scan(&Username, &UID, &GID, &Dir, &Shell)
		// log.Println("Student: ", Username, " ", UID, " ", GID, " ", Dir, " ", Shell)
		dbtest_passwd = append(dbtest_passwd,
			Passwd{
				Username: Username,
				Password: "x",
				UID:      UID,
				GID:      GID,
				Gecos:    "Test user 1",
				Dir:      Dir,
				Shell:    Shell,
			},
		)
		// fmt.Printf("%+v \n", dbtest_passwd)
	}

	row, err = db.Query("SELECT * FROM groups ")
	if err != nil {
		log.Fatal(err)
	}
	defer row.Close()
	for row.Next() { // Iterate and fetch the records from result cursor
		var Groupname string
		var GID uint
		var Members string
		row.Scan(&Groupname, &GID, &Members)
		// fmt.Println(Members)
		split := strings.Split(Members, ",")
		split = delete_empty(split)
		// fmt.Println(split)
		Slack := []string{}
		for i := 0; i < len(split); i++ {
			// ustString := strings.Join(split[i], " ")
			// fmt.Println(len(split))
			// fmt.Println(split[i])
			Slack = append(Slack, split[i])
		}

		dbtest_group = append(dbtest_group,
			Group{
				Groupname: Groupname,
				Password:  "x",
				GID:       GID,
				Members:   Slack,
			},
		)
	}
	// fmt.Printf("%+v \n", dbtest_group)

	dbtest_shadow = append(dbtest_shadow,
		Shadow{
			Username:        "Srtyu",
			Password:        "$6$yZcX.DOY$7bgsJhILMYl3DfMZsYUwoObbVt5Sj9FuujuhVn05Vg9hk.2AXLNy6o1DcPNq0SIyaRZ5YBZer2rYaycuh3qtg1", // Password is "password"
			LastChange:      17920,
			MinChange:       0,
			MaxChange:       99999,
			PasswordWarn:    7,
			InactiveLockout: -1,
			ExpirationDate:  -1,
			Reserved:        -1,
		},
		Shadow{
			Username:        "web",
			Password:        "$6$yZcX.DOY$7bgsJhILMYl3DfMZsYUwoObbVt5Sj9FuujuhVn05Vg9hk.2AXLNy6o1DcPNq0SIyaRZ5YBZer2rYaycuh3qtg1", // Password is "password"
			LastChange:      17920,
			MinChange:       0,
			MaxChange:       99999,
			PasswordWarn:    7,
			InactiveLockout: 0,
			ExpirationDate:  0,
			Reserved:        -1,
		},
	)

 

nss.go

  • nss.go contains the methods implemented by go-libnss modules
  • these methods will be querying the struct DBs (dbtest_passwd, dbtest_group) created by db.go
  • Will query is successful, go-libnss response code StatusSuccess is returned along with the details
package main

import (
	. "github.com/protosam/go-libnss"
	. "github.com/protosam/go-libnss/structs"
)

// Placeholder main() stub is neccessary for compile.
func main() {}

func init() {
	// We set our implementation to "TestImpl", so that go-libnss will use the methods we create
	SetImpl(TestImpl{})
}

// We're creating a struct that implements LIBNSS stub methods.
type TestImpl struct{ LIBNSS }

////////////////////////////////////////////////////////////////
// Passwd Methods
////////////////////////////////////////////////////////////////

// PasswdAll() will populate all entries for libnss
func (self TestImpl) PasswdAll() (Status, []Passwd) {
	if len(dbtest_passwd) == 0 {
		return StatusUnavail, []Passwd{}
	}
	return StatusSuccess, dbtest_passwd
}

// PasswdByName() returns a single entry by name.
func (self TestImpl) PasswdByName(name string) (Status, Passwd) {
	for _, entry := range dbtest_passwd {
		if entry.Username == name {
			return StatusSuccess, entry
		}
	}
	return StatusNotfound, Passwd{}
}

// PasswdByUid() returns a single entry by uid.
func (self TestImpl) PasswdByUid(uid uint) (Status, Passwd) {
	for _, entry := range dbtest_passwd {
		if entry.UID == uid {
			return StatusSuccess, entry
		}
	}
	return StatusNotfound, Passwd{}
}

////////////////////////////////////////////////////////////////
// Group Methods
////////////////////////////////////////////////////////////////
// endgrent
func (self TestImpl) GroupAll() (Status, []Group) {
	if len(dbtest_group) == 0 {
		return StatusUnavail, []Group{}
	}
	return StatusSuccess, dbtest_group
}

// getgrent
func (self TestImpl) GroupByName(name string) (Status, Group) {
	for _, entry := range dbtest_group {
		if entry.Groupname == name {
			return StatusSuccess, entry
		}
	}
	return StatusNotfound, Group{}
}

// getgrnam
func (self TestImpl) GroupByGid(gid uint) (Status, Group) {
	for _, entry := range dbtest_group {
		if entry.GID == gid {
			return StatusSuccess, entry
		}
	}
	return StatusNotfound, Group{}
}

////////////////////////////////////////////////////////////////
// Shadow Methods
////////////////////////////////////////////////////////////////
// endspent
func (self TestImpl) ShadowAll() (Status, []Shadow) {
	if len(dbtest_shadow) == 0 {
		return StatusUnavail, []Shadow{}
	}
	return StatusSuccess, dbtest_shadow
}

// getspent
func (self TestImpl) ShadowByName(name string) (Status, Shadow) {
	for _, entry := range dbtest_shadow {
		if entry.Username == name {
			return StatusSuccess, entry
		}
	}
	return StatusNotfound, Shadow{}
}

Compilation and Testing

  • Below command can be used to compile libnss_ldh shared object binary  file . The created file need to be placed in /usr/lib64 path
root@LDH]# CGO_CFLAGS="-g -O2 -D __LIB_NSS_NAME=ldh" go build --buildmode=c-shared -o libnss_ldh.so.2 nss.go db.go ;
root@LDH]# cp libnss_ldh.so.2 /usr/lib64/
  • One the binary is placed in the path, we can simply do id or getent command to see if keycloak users are visible in our linux machine
  • I had created user casy and it is part of groups casy and dummy. Same is visible when we do id or getent to passwd and group
[root@3vcpu-2 NSS]# id casy
uid=8795(casy) gid=2345(casy) groups=2345(casy)
[root@3vcpu-2 NSS]# getent group casy
casy:x:2345:casy,dummy
[root@3vcpu-2 NSS]# getent passwd casy
casy:x:8795:2345:Test user 1:/home/bg:/bin/bash

 

Search on LinuxDataHub

Leave a Comment