From 5a7b87d1abfec09384137b7bf5cf9da906d7dd82 Mon Sep 17 00:00:00 2001 From: Slack Coder Date: Mon, 7 Jun 2021 13:43:31 -0500 Subject: initial commit --- github.go | 281 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 281 insertions(+) create mode 100644 github.go (limited to 'github.go') diff --git a/github.go b/github.go new file mode 100644 index 0000000..ec478d4 --- /dev/null +++ b/github.go @@ -0,0 +1,281 @@ +package main + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "net/http" + "os" + "os/exec" + "os/user" + osuser "os/user" + "path" + "strconv" + "strings" +) + +var UsernameFile = ".github-id" + +var AuthorizedKeys = ".ssh/authorized_keys" + +const SSHDirPermissions = 0700 +const AuthorizedKeysPermissions = 0644 + +// KeyIdentifier tells which SSH keys belong to this tool. +const KeyIdentifier = "Github Key #" + +// TODO: Return error for missing User in Fetch +// TODO: Overwrite keys file only when necessary +// TODO: Double check errors +// TODO: Update README +// TODO: Allow overriding UsernameFile +// TODO: Allow AuthorizedKeys locations + +type LocalUser string + +func (u *LocalUser) String() string { + return string(*u) +} + +type GithubUser string + +func (u *GithubUser) String() string { + return string(*u) +} + +type GithubKey struct { + Key string + UserName GithubUser + ID int +} + +func (g GithubKey) SSHKey() string { + return fmt.Sprintf("%s %s's Github Key #%d", g.Key, g.UserName, g.ID) +} + +func (g GithubKey) String() string { + return g.SSHKey() +} + +func fetchKeys(username GithubUser) ([]GithubKey, error) { + url := fmt.Sprintf("https://api.github.com/users/%s/keys", username) + resp, err := http.Get(url) + if err != nil { + return nil, err + } + + if !(resp.StatusCode >= 200 && resp.StatusCode < 300) { + return nil, fmt.Errorf("bad response %s", resp.Status) + } + + var keys []GithubKey + err = json.NewDecoder(resp.Body).Decode(&keys) + if err != nil { + return nil, err + } + for i, _ := range keys { + keys[i].UserName = username + } + return keys, nil +} + +func lookupGithubName(username LocalUser) (GithubUser, error) { + u, err := osuser.Lookup(username.String()) + if err != nil { + return GithubUser(""), err + } + idFilePath := path.Join(u.HomeDir, UsernameFile) + buf, err := ioutil.ReadFile(idFilePath) + if err != nil { + return GithubUser(""), err + } + name := strings.TrimSpace(string(buf)) + return GithubUser(name), err +} + +func members(groupName string) ([]LocalUser, error) { + group, err := user.LookupGroup(groupName) + if err != nil { + return nil, err + } + + cmd := exec.Command("getent", "passwd") + var stdout bytes.Buffer + cmd.Stdout = &stdout + cmd.Stderr = nil + err = cmd.Run() + if err != nil { + return nil, err + } + passwdEntries := strings.Split(string(stdout.Bytes()), "\n") + + var members []LocalUser + for _, entry := range passwdEntries { + if len(entry) == 0 { + continue + } + + fields := strings.Split(entry, ":") + userName := fields[0] + systemUser, err := user.Lookup(userName) + if err != nil { + return nil, err + } + groupIds, err := systemUser.GroupIds() + if err != nil { + return nil, err + } + for _, gid := range groupIds { + if gid == group.Gid { + members = append(members, LocalUser(userName)) + break + } + } + } + return members, nil +} + +func syncLocalKeys(user LocalUser, keys ...GithubKey) error { + u, err := osuser.Lookup(user.String()) + if err != nil { + return err + } + userUid, err := strconv.Atoi(u.Uid) + if err != nil { + return errors.New("POSIX system expected") + } + userGid, err := strconv.Atoi(u.Gid) + if err != nil { + return errors.New("POSIX system expected") + } + + var origKeys []string + ak := path.Join(u.HomeDir, AuthorizedKeys) + if _, err := os.Stat(ak); err == nil { + buf, err := ioutil.ReadFile(ak) + if err != nil { + return err + } + origKeys = strings.Split(string(buf), "\n") + } + + // TODO: check duplicate keys + var newKeys []string + for _, key := range origKeys { + if len(key) == 0 { + continue + } + if strings.Contains(key, KeyIdentifier) { + continue + } + newKeys = append(newKeys, key) + } + for _, key := range keys { + newKeys = append(newKeys, key.SSHKey()) + } + output := strings.Join(newKeys, "\n") + + if _, err := os.Stat(path.Dir(ak)); os.IsNotExist(err) { + dir := path.Dir(ak) + err = os.MkdirAll(dir, SSHDirPermissions) + if err != nil { + return fmt.Errorf("could not create SSH folder: %w", err) + } + // ignore errors, root should not be required + _ = os.Chown(dir, userUid, userGid) + } + if _, err := os.Stat(path.Dir(ak)); os.IsNotExist(err) { + f, err := os.Create(ak) + if err != nil { + return fmt.Errorf("could not create create AuthorizedKeys file: %w", err) + } + _ = f.Chown(userUid, userGid) + _ = f.Chmod(AuthorizedKeysPermissions) + _ = f.Close() + } + return ioutil.WriteFile(ak, []byte(output), AuthorizedKeysPermissions) +} + +func failOnErr(msg string, err error) { + if err != nil { + fmt.Fprintf(os.Stderr, "%s: %s\n", msg, err.Error()) + os.Exit(1) + } +} + +func Fetch(args ...string) { + var users []GithubUser + for _, user := range args { + users = append(users, GithubUser(user)) + } + if len(args) == 0 { + user, err := osuser.Current() + failOnErr("could not find current user", err) + githubUser, err := lookupGithubName(LocalUser(user.Username)) + failOnErr("could not determine GitHub account", err) + users = append(users, githubUser) + } + + var allKeys []GithubKey + for _, user := range users { + keys, err := fetchKeys(user) + if err != nil { + failOnErr("could not fetch SSH keys", err) + } + allKeys = append(allKeys, keys...) + } + + for _, key := range allKeys { + fmt.Println(key) + } +} + +func SyncUsers(args ...string) { + var localUsers []LocalUser + for _, arg := range args { + localUsers = append(localUsers, LocalUser(arg)) + } + if len(localUsers) == 0 { + u, err := osuser.Current() + failOnErr("could not find current user", err) + localUsers = append(localUsers, LocalUser(u.Username)) + } + + for _, user := range localUsers { + githubID, err := lookupGithubName(user) + if err != nil { + // skip + continue + } + keys, err := fetchKeys(githubID) + if err != nil { + failOnErr("could not fetch SSH keys", err) + } + err = syncLocalKeys(user, keys...) + if err != nil { + failOnErr("could not update AuthorizedKeys file", err) + } + } +} + +func SyncGroups(args ...string) { + var groupNames []string + if len(args) == 0 { + groupNames = append(groupNames, "users") + } + + var users []LocalUser + for _, groupName := range groupNames { + groupsUsers, err := members(groupName) + failOnErr("vould not determine membership", err) + users = append(users, groupsUsers...) + } + + var syncUserArgs []string + for _, val := range users { + syncUserArgs = append(syncUserArgs, string(val)) + } + SyncUsers(syncUserArgs...) +} -- cgit v1.2.3