Table of Contents
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
Note: shadow will not be updated, as I'm not planning to store any passwords in SQL DB , and for PAM authentication, if required i will be using a PAM module for keycloak which is discussed in another article.
User look up flows
Below diagram shows the nss lookup for passwd and group database
Note: It is possible to implement a solution without using SQL DB and by connecting directly to keycloak, but that is not in scope of this article
- 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
- Rocky Linux 8 VM on VirtualBox
- RAM 4 GB
- 2 VCPUs
- 23 GB HD
- Host OS: Windows 10
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
Note: It is not recommended to use admin-cli client as it is having admin privileges, rather create a separate client for user and group query with right privileges.
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,
},
)
Note: dbtest_shadow shouldn't be hardcoded rather, same also need to be generated from keycloak, if you are planning to use su or ssh . As linux PAM expects entry to be shadow file, even if we use keycloak PAM module
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
Warning: As we are playing around with modules which are a key component of user login to a machine . It is recommended to not to test on production machines.
This article need to be considered as tutorial only. Any issue (machines going down) caused by the implementation done following this article will not responsible by the author or Linuxdatahub owners
This article need to be considered as tutorial only. Any issue (machines going down) caused by the implementation done following this article will not responsible by the author or Linuxdatahub owners