aboutsummaryrefslogtreecommitdiff
path: root/github.go
diff options
context:
space:
mode:
Diffstat (limited to 'github.go')
-rw-r--r--github.go281
1 files changed, 281 insertions, 0 deletions
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...)
+}