diff --git a/.gitignore b/.gitignore index ca93d74..0f57d47 100644 --- a/.gitignore +++ b/.gitignore @@ -122,3 +122,4 @@ Temporary Items *.ldif user_vhost_list.conf userVhost +rb-ldap diff --git a/README.md b/README.md index 6d6f695..62a5a36 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,26 @@ -# userVhost +# rb-ldap -Script to query ldap for user info to generate apache template conf for user -vhosts. +[![Go Report Card](https://goreportcard.com/badge/github.com/redbrick/rbldap)](https://goreportcard.com/report/github.com/redbrick/rbldap) + +Script to interact with Redbrick LDAP. + +* query ldap for user info to generate apache template conf for user vhosts. +* Search for users in ldap ## Installation ```console -go get github.com/redbrick/userVhost +go get github.com/redbrick/rbldap/cmd/rb-ldap ``` ## Run ```console -userVhost ./ldap.secret +rb-ldap ``` -The conf will be output to the current dir. Run `userVhost -h` to get a list of -flags +Run `rb-ldap -h` to get a list of flags and commands. + +## Notes + +The conf from `rb-ldag g` will be output to the current dir. diff --git a/cmd/rb-ldap/main.go b/cmd/rb-ldap/main.go new file mode 100644 index 0000000..ccf3272 --- /dev/null +++ b/cmd/rb-ldap/main.go @@ -0,0 +1,129 @@ +package main + +import ( + "fmt" + "os" + + "github.com/redbrick/rbldap/internal/pkg/rbldap" + "github.com/urfave/cli" +) + +func main() { + app := cli.NewApp() + app.Name = "rb-ldap" + app.Usage = "Command line interface for Redbrick Ldap" + app.ArgsUsage = "" + app.HideVersion = true + app.EnableBashCompletion = true + + app.Flags = []cli.Flag{ + cli.StringFlag{ + Name: "user, u", + Value: "cn=root,ou=ldap,o=redbrick", + Usage: "ldap user, used for authentication", + }, + + cli.StringFlag{ + Name: "dcu-user", + Value: "CN=rblookup,OU=Service Accounts,DC=ad,DC=dcu,DC=ie", + Usage: "Active Directory user for DCU, used for authentication", + }, + + cli.StringFlag{ + Name: "host", + Value: "ldap.internal", + Usage: "ldap host to query", + }, + + cli.StringFlag{ + Name: "dcu-host", + Value: "ad.dcu.ie", + Usage: "DCU Active Directory host to query", + }, + + cli.IntFlag{ + Name: "port, p", + Value: 389, + Usage: "Port for ldap host", + }, + + cli.IntFlag{ + Name: "dcu-port", + Value: 389, + Usage: "Port for DCU Active Directory host", + }, + + cli.StringFlag{ + Name: "password", + Usage: "password for the ldap server", + FilePath: "/etc/ldap.secret", + }, + + cli.StringFlag{ + Name: "dcu-password", + Usage: "password for the DCU ldap server", + FilePath: "/etc/dcu_ldap.secret", + }, + + cli.BoolFlag{ + Name: "dry-run", + Usage: "output to console rather then file", + }, + } + app.Commands = []cli.Command{ + { + Name: "generate", + Aliases: []string{"g"}, + Usage: "generate list for uservhost macro", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "conf, c", + Value: "./user_vhost_list.conf", + Usage: "Output configuration `FILE`", + }, + }, + Action: rbldap.Generate, + }, + { + Name: "search", + Aliases: []string{"s"}, + Usage: "Search ldap for user", + Flags: []cli.Flag{ + cli.StringFlag{ + Name: "mail, altmail", + Value: "*", + Usage: "User email", + }, + cli.StringFlag{ + Name: "user, u, uid, nick, username", + Value: "*", + Usage: "User username", + }, + cli.StringFlag{ + Name: "id", + Value: "*", + Usage: "DCU id Number", + }, + cli.StringFlag{ + Name: "name, fullname", + Value: "*", + Usage: "User's fullname", + }, + cli.BoolFlag{ + Name: "newbie, noob", + Usage: "filter for new users", + }, + cli.BoolFlag{ + Name: "dcu, DCU", + Usage: "Query DCU Active Directory", + }, + }, + Action: rbldap.Search, + }, + } + + if err := app.Run(os.Args); err != nil { + fmt.Fprintf(os.Stderr, "error: %v\n", err) + os.Exit(1) + } +} diff --git a/internal/pkg/rbldap/generate.go b/internal/pkg/rbldap/generate.go new file mode 100644 index 0000000..c4609ae --- /dev/null +++ b/internal/pkg/rbldap/generate.go @@ -0,0 +1,42 @@ +package rbldap + +import ( + "fmt" + "os" + "strings" + + "github.com/redbrick/rbldap/pkg/rbuser" + "github.com/urfave/cli" +) + +// Generate takes cli context and generates user vhost for rbuser +func Generate(ctx *cli.Context) error { + rb, err := rbuser.NewRbLdap( + ctx.String("user"), + ctx.String("password"), + ctx.String("host"), + ctx.Int("port"), + ) + if err != nil { + return err + } + vhosts, err := rb.Generate() + if err != nil { + return err + } + if ctx.Bool("dry-run") { + fmt.Print(strings.Join(vhosts, "\n")) + return nil + } + file, err := os.Create(ctx.String("conf")) + if err != nil { + return err + } + defer file.Close() + n, err := file.WriteString(strings.Join(vhosts, "\n")) + if err != nil { + return err + } + fmt.Printf("wrote %d bytes %s\n", n, ctx.String("conf")) + return file.Sync() +} diff --git a/internal/pkg/rbldap/search.go b/internal/pkg/rbldap/search.go new file mode 100644 index 0000000..f298c79 --- /dev/null +++ b/internal/pkg/rbldap/search.go @@ -0,0 +1,90 @@ +package rbldap + +import ( + "bytes" + "fmt" + "os" + "regexp" + + "github.com/redbrick/rbldap/pkg/rbuser" + "github.com/urfave/cli" +) + +// Search command for cli app +func Search(ctx *cli.Context) error { + if ctx.NArg() != 0 { + fmt.Fprintf(os.Stderr, "\n") + return fmt.Errorf("Missing required arguments") + } + mail := filter("altmail", ctx.String("mail")) + id := filter("id", ctx.String("id")) + re := regexp.MustCompile(`\ `) + name := re.ReplaceAllString(ctx.String("name"), `*$1*$2*`) + if ctx.Bool("dcu") { + dcu, err := rbuser.NewDcuLdap( + ctx.String("dcu-user"), + ctx.String("dcu-password"), + ctx.String("dcu-host"), + ctx.Int("dcu-port"), + ) + if err != nil { + return err + } + user, searchErr := dcu.Search(filterAnd( + filter("displayName", name), + filter("cn", ctx.String("user")), + id), + ) + if searchErr != nil { + return searchErr + } + return user.PrettyPrint() + } + rb, err := rbuser.NewRbLdap( + ctx.String("user"), + ctx.String("password"), + ctx.String("host"), + ctx.Int("port"), + ) + if err != nil { + return err + } + noob := "" + if ctx.Bool("noob") { + noob = "(newbie=TRUE)" + } + user, err := rb.Search(filterAnd( + filter("cn", name), + filterOr( + filter("uid", ctx.String("user")), + filter("gecos", ctx.String("user")), + ), id, mail, noob), + ) + if err != nil { + return err + } + return user.PrettyPrint() +} + +func filter(key, search string) string { + return fmt.Sprintf("(%s=%s)", key, search) +} + +func filterAnd(args ...string) string { + return filterJoin("&", args) +} + +func filterOr(args ...string) string { + return filterJoin("|", args) +} + +func filterJoin(join string, args []string) string { + var buffer bytes.Buffer + buffer.WriteString("(") + buffer.WriteString(join) + for _, filter := range args { + buffer.WriteString(filter) + } + buffer.WriteString(")") + return buffer.String() +} diff --git a/ldap.go b/ldap.go deleted file mode 100644 index 77ab355..0000000 --- a/ldap.go +++ /dev/null @@ -1,9 +0,0 @@ -package main - -// LdapConf server object used for connecting to server -type LdapConf struct { - User string - Password string - Host string - Port int -} diff --git a/ldif/index.js b/ldif/index.js index 4caa74b..0723a2c 100644 --- a/ldif/index.js +++ b/ldif/index.js @@ -10,6 +10,37 @@ const fs = require('fs-extra'); const { isEmpty, isUndefined } = require('lodash'); const ldif = require('ldif'); +const vhost = user => + `use VHost /storage/webtree/${user.username.charAt(0)}/${user.username} ${user.username} ${ + user.group + } ${user.username}\n`; + +const ldap = ldif.parseFile('./entry.ldif'); +fs.appendFile( + 'user_vhost_list.conf', + ldap.entries + .map(convertUser) + .filter(u => !isUndefined(u)) + .join('\n'), +); + +function convertUser(entry) { + const { attributes: { objectClass, uid, gidNumber } } = entry.toObject(); + const user = { username: uid }; + if ( + objectClass === 'club' || + objectClass === 'committe' || + objectClass === 'society' || + objectClass === 'associate' || + objectClass === 'member' + ) { + user.group = objectClass; + } + if (isEmpty(user.group)) user.group = getGroup(gidNumber); + if (!isEmpty(user.username) && !isEmpty(user.group)) return vhost(user); + return undefined; +} + function getGroup(gid) { switch (gid) { case 100: @@ -38,32 +69,3 @@ function getGroup(gid) { return 'member'; } } - -const vhost = user => - `use VHost /storage/webtree/${user.username.charAt(0)}/${user.username} ${user.username} ${ - user.group - } ${user.username}\n`; - -const ldap = ldif.parseFile('./entry.ldif'); -fs.appendFile( - 'user_vhost_list.conf', - ldap.entries - .map(entry => { - const { attributes: { objectClass, uid, gidNumber } } = entry.toObject(); - const user = { username: uid }; - if ( - objectClass === 'club' || - objectClass === 'committe' || - objectClass === 'society' || - objectClass === 'associate' || - objectClass === 'member' - ) { - user.group = objectClass; - } - if (isEmpty(user.group)) user.group = getGroup(gidNumber); - if (!isEmpty(user.username) && !isEmpty(user.group)) return vhost(user); - return undefined; - }) - .filter(u => !isUndefined(u)) - .join('\n'), -); diff --git a/main.go b/main.go deleted file mode 100644 index 5deb679..0000000 --- a/main.go +++ /dev/null @@ -1,82 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "github.com/urfave/cli" -) - -func main() { - app := cli.NewApp() - app.Name = "userVhost" - app.Usage = "apache conf generator for ldap" - app.ArgsUsage = "" - app.HideVersion = true - app.EnableBashCompletion = true - - app.Flags = []cli.Flag{ - cli.StringFlag{ - Name: "user, u", - Value: "cn=root,ou=ldap,o=redbrick", - Usage: "ldap user, used for authentication", - }, - - cli.StringFlag{ - Name: "host", - Value: "ldap.internal", - Usage: "ldap host to query", - }, - - cli.IntFlag{ - Name: "port, p", - Value: 389, - Usage: "Port for ldap host", - }, - - cli.StringFlag{ - Name: "password", - Usage: "password for the ldap server", - FilePath: "/etc/ldap.secret", - }, - } - app.Commands = []cli.Command{ - { - Name: "generate", - Aliases: []string{"g"}, - Usage: "generate list for uservhost macro", - Flags: []cli.Flag{ - cli.StringFlag{ - Name: "conf, c", - Value: "./user_vhost_list.conf", - Usage: "File to output conf too", - }, - }, - Action: func(ctx *cli.Context) error { - if ctx.NArg() != 0 { - fmt.Fprintf(os.Stderr, "\n") - return fmt.Errorf("Missing required arguments") - } - - ldap := LdapConf{ - User: ctx.String("user"), - Password: ctx.String("password"), - Host: ctx.String("host"), - Port: ctx.Int("port"), - } - n, err := Generate(ldap, ctx.String("path")) - if err != nil { - return err - } - fmt.Printf("wrote %d bytes ./user_vhost_list.conf\n", n) - return nil - }, - }, - } - - err := app.Run(os.Args) - if err != nil { - fmt.Fprintf(os.Stderr, "error: %v\n", err) - os.Exit(1) - } -} diff --git a/pkg/rbuser/dculdap.go b/pkg/rbuser/dculdap.go new file mode 100644 index 0000000..557b1a3 --- /dev/null +++ b/pkg/rbuser/dculdap.go @@ -0,0 +1,54 @@ +package rbuser + +import ( + "fmt" + "regexp" + "strconv" + + ldap "gopkg.in/ldap.v2" +) + +// DcuLdap Server object used for connecting to DCU AD +type DcuLdap struct { + *ldapConf +} + +// NewDcuLdap create ldap connection to DCU AD +func NewDcuLdap(user, password, host string, port int) (*DcuLdap, error) { + conf := &ldapConf{user: user, password: password, host: host, port: port} + dcu := DcuLdap{conf} + return &dcu, dcu.connect() +} + +// Search dcu ldap for a given filter and return first user that matches +func (dcu *DcuLdap) Search(filter string) (RbUser, error) { + sr, err := dcu.Conn.Search(ldap.NewSearchRequest( + "o=ad,o=dcu,o=ie", + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, + 0, 0, false, filter, + []string{"employeeNumber", "givenName", "sn", "gecos", "mail", "l"}, nil, + )) + if err != nil { + return RbUser{}, err + } + for _, entry := range sr.Entries { + dcuID, _ := strconv.Atoi(entry.GetAttributeValue("employeeNumber")) + course, year := splitCourseYear(entry.GetAttributeValue("l")) + return RbUser{ + CN: fmt.Sprintf("%s %s", entry.GetAttributeValue("givenname"), entry.GetAttributeValue("sn")), + Altmail: entry.GetAttributeValue("mail"), + ID: dcuID, + Course: course, + Year: year, + }, nil + } + return RbUser{}, err +} + +func splitCourseYear(courseYear string) (string, int) { + r, _ := regexp.Compile("([A-Z]+)") + rYear, _ := regexp.Compile("([0-9]+)") + + year, _ := strconv.Atoi(rYear.FindString(courseYear)) + return r.FindString(courseYear), year +} diff --git a/generate.go b/pkg/rbuser/generate.go similarity index 53% rename from generate.go rename to pkg/rbuser/generate.go index b83ee9c..c402747 100644 --- a/generate.go +++ b/pkg/rbuser/generate.go @@ -1,27 +1,14 @@ -package main +package rbuser import ( - "fmt" - "os" "strconv" - "strings" ldap "gopkg.in/ldap.v2" ) // Generate user vhost conf from ldap -func Generate(conf LdapConf, output string) (int, error) { - l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", conf.Host, conf.Port)) - if err != nil { - return 0, err - } - defer l.Close() - - err = l.Bind(conf.User, conf.Password) - if err != nil { - return 0, err - } - +func (l *RbLdap) Generate() ([]string, error) { + var vhosts []string searchRequest := ldap.NewSearchRequest( "ou=accounts,o=redbrick", ldap.ScopeSingleLevel, ldap.NeverDerefAliases, @@ -29,40 +16,25 @@ func Generate(conf LdapConf, output string) (int, error) { []string{"objectClass", "uid", "gidNumber"}, nil, ) - sr, err := l.Search(searchRequest) + sr, err := l.Conn.Search(searchRequest) if err != nil { - return 0, err + return vhosts, err } - var vhosts []string for _, entry := range sr.Entries { - uid := entry.GetAttributeValue("uid") group := entry.GetAttributeValue("objectClass") if group == "" { - gidNum, conversionErr := strconv.Atoi(entry.GetAttributeValue("gidNumber")) - if conversionErr != nil { - return 0, conversionErr + gidNum, err := strconv.Atoi(entry.GetAttributeValue("gidNumber")) + if err != nil { + return vhosts, err } group = gidToGroup(gidNum) } if group != "" && group != "redbrick" && group != "reserved" { - u := rbUser{uid, []rune(uid)[0], group} + u := RbUser{UID: entry.GetAttributeValue("uid"), ObjectClass: group} vhosts = append(vhosts, u.Vhost()) } } - f, err := os.Create(output) - if err != nil { - return 0, err - } - defer f.Close() - if err != nil { - return 0, err - } - n, err := f.WriteString(strings.Join(vhosts, "\n")) - if err != nil { - return n, err - } - err = f.Sync() - return n, err + return vhosts, nil } func gidToGroup(gid int) string { @@ -73,8 +45,6 @@ func gidToGroup(gid int) string { return "society" case 102: return "club" - case 103: - return "member" case 105: return "founder" case 107: diff --git a/pkg/rbuser/ldap.go b/pkg/rbuser/ldap.go new file mode 100644 index 0000000..af6d39c --- /dev/null +++ b/pkg/rbuser/ldap.go @@ -0,0 +1,27 @@ +package rbuser + +import ( + "fmt" + + ldap "gopkg.in/ldap.v2" +) + +// LdapConf Server object used for connecting to server +type ldapConf struct { + user string + password string + host string + port int + Conn *ldap.Conn +} + +// Connect to ldap database +func (conf *ldapConf) connect() error { + l, err := ldap.Dial("tcp", fmt.Sprintf("%s:%d", conf.host, conf.port)) + if err != nil { + return err + } + conf.Conn = l + defer conf.Conn.Close() + return conf.Conn.Bind(conf.user, conf.password) +} diff --git a/pkg/rbuser/rbldap.go b/pkg/rbuser/rbldap.go new file mode 100644 index 0000000..4309b93 --- /dev/null +++ b/pkg/rbuser/rbldap.go @@ -0,0 +1,13 @@ +package rbuser + +// RbLdap Server object used for connecting to server +type RbLdap struct { + *ldapConf +} + +// NewRbLdap create ldap connection to Redbrick LDAP +func NewRbLdap(user, password, host string, port int) (*RbLdap, error) { + conf := &ldapConf{user: user, password: password, host: host, port: port} + rb := RbLdap{conf} + return &rb, rb.connect() +} diff --git a/pkg/rbuser/rbuser.go b/pkg/rbuser/rbuser.go new file mode 100644 index 0000000..41f9bc3 --- /dev/null +++ b/pkg/rbuser/rbuser.go @@ -0,0 +1,74 @@ +package rbuser + +import ( + "fmt" + "html/template" + "os" + "time" +) + +// RbUser is the redbric ldap user +type RbUser struct { + UID string + UserType string + ObjectClass string + Newbie bool // New this year + CN string // Full name + Altmail string // Alternate email + ID int // DCU ID number + Course string // DCU course code + Year int // DCU course year number/code + YearsPaid int // Number of years paid (integer) + Updatedby string // Username of user last to update + Updated time.Time + CreatedBy string // Username of user that created them + Created time.Time + Birthday time.Time + UIDNumber int + GidNumber int + Gecos string + LoginShell string + HomeDirectory string + UserPassword string // Crypted password. + Host []string // List of hosts. + ShadowLastChange time.Time +} + +// Vhost reutrn apache macro template +func (u *RbUser) Vhost() string { + initial := []rune(u.UID)[0] + return fmt.Sprintf("use VHost /storage/webtree/%s/%s %s %s %s", string(initial), u.UID, u.UID, u.ObjectClass, u.UID) +} + +// PrettyPrint output user info to command line +func (u *RbUser) PrettyPrint() error { + const output = ` + User Information + ================ + {{ with .UID }}uid: {{ . }} + {{ end }}{{ with .UserType }}usertype: {{ . }} + {{ end }}{{ with .ObjectClass }}objectClass: {{ . }} + {{ end }}{{ with .Newbie }}newbie: {{ . }} + {{ end }}{{ with .CN }}cn: {{ . }} + {{ end }}{{ with .Altmail }}altmail: {{ . }} + {{ end }}{{ with .ID }}id: {{ . }} + {{ end }}{{ with .Course }}course: {{ . }} + {{ end }}{{ with .Year }}year: {{ . }} + {{ end }}{{ with .YearsPaid }}yearsPaid: {{ . }} + {{ end }}{{ with .Updatedby }}updatedby: {{ . }} + {{ end }}{{ with .Updated }}updated: {{ . }} + {{ end }}{{ with .CreatedBy }}createdby: {{ . }} + {{ end }}{{ with .Created }}created: {{ . }} + {{ end }}{{ with .UIDNumber }} uidNumber: {{ . }} + {{ end }}{{ with .GIDNumber }}gidNumber: {{ . }} + {{ end }}{{ with .Gecos }}gecos: {{ . }} + {{ end }}{{ with .LoginShell }}loginShell: {{ . }}{{ end }} + {{ end }}{{ with .HomeDirectory }}homeDirectory: {{ . }}{{ end }} + {{ end }}{{ with .UserPassword }}userPassword: {SHA}{{ . }}{{ end }} + {{ end }}{{ with .Host }}host: {{ . }} + {{ end }}{{ with .ShadowLastChange }}shadowLastChange: {{ . }}{{ end }} +` + + t := template.Must(template.New("user").Parse(output)) + return t.Execute(os.Stdout, u) +} diff --git a/pkg/rbuser/search.go b/pkg/rbuser/search.go new file mode 100644 index 0000000..53df940 --- /dev/null +++ b/pkg/rbuser/search.go @@ -0,0 +1,62 @@ +package rbuser + +import ( + "strconv" + "strings" + "time" + + ldap "gopkg.in/ldap.v2" +) + +// Search ldap for a given filter and return first user that matches +func (rb *RbLdap) Search(filter string) (RbUser, error) { + sr, err := rb.Conn.Search(ldap.NewSearchRequest( + "ou=accounts,o=redbrick", + ldap.ScopeSingleLevel, ldap.NeverDerefAliases, + 0, 0, false, filter, + []string{"objectClass", "uid", "newbie", "cn", "altmail", "id", "course", "year", + "yearsPaid", "updatedBy", "updated", "createdBy", "created", "birthday", "uidNumber", + "gidNumber", "gecos", "loginShell", "homeDirectory", "userPassword", "host", + "shadowLastChange"}, nil, + )) + if err != nil { + return RbUser{}, err + } + for _, entry := range sr.Entries { + noob, _ := strconv.ParseBool(entry.GetAttributeValue("newbie")) + dcuID, _ := strconv.Atoi(entry.GetAttributeValue("id")) + year, _ := strconv.Atoi(entry.GetAttributeValue("year")) + yearsPaid, _ := strconv.Atoi(entry.GetAttributeValue("yearsPaid")) + uidNum, _ := strconv.Atoi(entry.GetAttributeValue("uidNumber")) + gidNum, _ := strconv.Atoi(entry.GetAttributeValue("gidNumber")) + updated, _ := time.Parse("2006-01-02 15:04:00", entry.GetAttributeValue("updated")) + shadow, _ := time.Parse("2006-01-02 15:04:00", entry.GetAttributeValue("shadowLastChange")) + created, _ := time.Parse("2006-01-02 15:04:00", entry.GetAttributeValue("created")) + birthday, _ := time.Parse("2006-01-02 15:04:00", entry.GetAttributeValue("birthday")) + return RbUser{ + UID: entry.GetAttributeValue("uid"), + ObjectClass: entry.GetAttributeValue("objectClass"), + Newbie: noob, + CN: entry.GetAttributeValue("cn"), + Altmail: entry.GetAttributeValue("altmail"), + ID: dcuID, + Course: entry.GetAttributeValue("course"), + Year: year, + YearsPaid: yearsPaid, + Updatedby: entry.GetAttributeValue("updatedBy"), + Updated: updated, + CreatedBy: entry.GetAttributeValue("createdBy"), + Created: created, + Birthday: birthday, + UIDNumber: uidNum, + GidNumber: gidNum, + Gecos: entry.GetAttributeValue("gecos"), + LoginShell: entry.GetAttributeValue("loginShell"), + HomeDirectory: entry.GetAttributeValue("homeDirectory"), + UserPassword: entry.GetAttributeValue("userPassword"), + Host: strings.Split(entry.GetAttributeValue("host"), ","), + ShadowLastChange: shadow, + }, nil + } + return RbUser{}, err +} diff --git a/rbUser.go b/rbUser.go deleted file mode 100644 index be0b64c..0000000 --- a/rbUser.go +++ /dev/null @@ -1,13 +0,0 @@ -package main - -import "fmt" - -type rbUser struct { - Name string - Initial rune - Group string -} - -func (u *rbUser) Vhost() string { - return fmt.Sprintf("use VHost /storage/webtree/%s/%s %s %s %s", string(u.Initial), u.Name, u.Name, u.Group, u.Name) -}