diff --git a/Documentation/cloud-config.md b/Documentation/cloud-config.md index f6c211d..837d59e 100644 --- a/Documentation/cloud-config.md +++ b/Documentation/cloud-config.md @@ -33,6 +33,7 @@ All but the `passwd` and `ssh-authorized-keys` fields will be ignored if the use - **groups**: Add user to these additional groups - **no-user-group**: Boolean. Skip default group creation. - **ssh-authorized-keys**: List of public SSH keys to authorize for this user +- **coreos-ssh-import-github**: Authorize SSH keys from Github user - **system**: Create the user as a system user. No home directory will be created. - **no-log-init**: Boolean. Skip initialization of lastlog and faillog databases. diff --git a/initialize/config.go b/initialize/config.go index 4974b44..a902f99 100644 --- a/initialize/config.go +++ b/initialize/config.go @@ -78,6 +78,12 @@ func Apply(cfg CloudConfig, env *Environment) error { return err } } + if user.SSHImportGithubUser != "" { + log.Printf("Authorizing github user %s SSH keys for CoreOS user '%s'", user.SSHImportGithubUser, user.Name) + if err := SSHImportGithubUser(user.Name, user.SSHImportGithubUser); err != nil { + return err + } + } } } diff --git a/initialize/config_test.go b/initialize/config_test.go index 28a21e3..bd5f6b8 100644 --- a/initialize/config_test.go +++ b/initialize/config_test.go @@ -215,7 +215,7 @@ users: t.Errorf("Failed to parse no-create-home field") } - if user.PrimaryGroup != "things"{ + if user.PrimaryGroup != "things" { t.Errorf("Failed to parse primary-group field, got %q", user.PrimaryGroup) } diff --git a/initialize/github.go b/initialize/github.go new file mode 100644 index 0000000..c8253ee --- /dev/null +++ b/initialize/github.go @@ -0,0 +1,52 @@ +package initialize + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + + "github.com/coreos/coreos-cloudinit/system" +) + +type GithubUserKey struct { + Id int `json:"id"` + Key string `json:"key"` +} + +func fetchGithubKeys(github_url string) ([]string, error) { + res, err := http.Get(github_url) + defer res.Body.Close() + if err != nil { + return nil, err + } + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return nil, err + } + var data []GithubUserKey + err = json.Unmarshal(body, &data) + if err != nil { + return nil, err + } + keys := make([]string, 0) + for _, key := range data { + keys = append(keys, key.Key) + } + return keys, err + +} + +func SSHImportGithubUser(system_user string, github_user string) error { + url := fmt.Sprintf("https://api.github.com/users/%s/keys", github_user) + keys, err := fetchGithubKeys(url) + if err != nil { + return err + } + key_name := fmt.Sprintf("github-%s", github_user) + err = system.AuthorizeSSHKeys(system_user, key_name, keys) + if err != nil { + return err + } + return nil +} diff --git a/initialize/github_test.go b/initialize/github_test.go new file mode 100644 index 0000000..9b6a603 --- /dev/null +++ b/initialize/github_test.go @@ -0,0 +1,71 @@ +package initialize + +import ( + "fmt" + "net/http" + "net/http/httptest" + "testing" +) + +func TestCloudConfigUsersGithubMarshal(t *testing.T) { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + gh_res := ` +[ + { + "id": 67057, + "key": "ssh-dss AAAAB3NzaC1kc3MAAACBAIHAu822ggSkIHrJYvhmBceOSVjuflfQm8RbMMDNVe9relQfuPbN+nxGGTCKzPLebeOcX+Wwi77TPXWwK3BZMglfXxhABlFPsuMb63Tqp94pBYsJdx/iFj9iGo6pKoM1k8ubOcqsUnq+BR9895zRbE7MjdwkGo67+QhCEwvkwAnNAAAAFQCuddVqXLCubzqnWmeHLQE+2GFfHwAAAIBnlXW5h15ndVuwi0htF4oodVSB1KwnTWcuBK+aE1zRs76yvRb0Ws+oifumThDwB/Tec6FQuAfRKfy6piChZqsu5KvL98I+2t5yyi1td+kMvdTnVL2lW44etDKseOcozmknCOmh4Dqvhl/2MwrDAhlPaN08EEq9h3w3mXtNLWH64QAAAIBAzDOKr17llngaKIdDXh+LtXKh87+zfjlTA36/9r2uF2kYE5uApDtu9sPCkt7+YBQt7R8prADPckwAiXwVdk0xijIOpLDBmoydQJJRQ+zTMxvpQmUr/1kUOv0zb+lB657CgvN0vVTmP2swPeMvgntt3C4vw7Ab+O+MS9peOAJbbQ==" + }, + { + "id": 3340477, + "key": "ssh-dss AAAAB3NzaC1kc3MAAACBANxpzIbTzKTeBRaOIdUxwwGwvDasTfU/PonhbNIuhYjc+xFGvBRTumox2F+luVAKKs4WdvA4nJXaY1OFi6DZftk5Bp4E2JaSzp8ulAzHsMexDdv6LGHGEJj/qdHAL1vHk2K89PpwRFSRZI8XRBLjvkr4ZgBKLG5ZILXPJEPP2j3lAAAAFQCtxoTnV8wy0c4grcGrQ+1sCsD7WQAAAIAqZsW2GviMe1RQrbZT0xAZmI64XRPrnLsoLxycHWlS7r6uUln2c6Ae2MB/YF0d4Kd1XZii9GHj7rrypqEo7MW8uSabhu70nmu1J8m2O3Dsr+4oJLeat9vwPsJV92IKO0jQwjKnAOHOiB9JKGeCw+NfXfogbti9/q38Q6XcS+SI5wAAAIEA1803Y2h+tOOpZXAsNIwl9mRfExWzLQ3L7knwJdznQu/6SW1H/1oyoYLebuk187Qj2UFI5qQ6AZNc49DvohWx0Cg6ABcyubNyoaCjZKWIdxVnItHWNbLe//+tyTu0I2eQwJOORsEPK5gMpf599C7wXQ//DzZOWbTWiHEX52gCTmk=" + }, + { + "id": 5224438, + "key": "ssh-dss AAAAB3NzaC1kc3MAAACBAPKRWdKhzGZuLAJL6M1eM51hWViMqNBC2C6lm2OqGRYLuIf1GJ391widUuSf4wQqnkR22Q9PCmAZ19XCf11wBRMnuw9I/Z3Bt5bXfc+dzFBCmHYGJ6wNSv++H9jxyMb+usmsenWOFZGNO2jN0wrJ4ay8Yt0bwtRU+VCXpuRLszMzAAAAFQDZUIuPjcfK5HLgnwZ/J3lvtvlUjQAAAIEApIkAwLuCQV5j3U6DmI/Y6oELqSUR2purFm8jo8jePFfe1t+ghikgD254/JXlhDCVgY0NLXcak+coJfGCTT23quJ7I5xdpTn/OZO2Q6Woum/bijFC/UWwQbLz0R2nU3DoHv5v6XHQZxuIG4Fsxa91S+vWjZFtI7RuYlBCZA//ANMAAACBAJO0FojzkX6IeaWLqrgu9GTkFwGFazZ+LPH5JOWPoPn1hQKuR32Uf6qNcBZcIjY7SF0P7HF5rLQd6zKZzHqqQQ92MV555NEwjsnJglYU8CaaZsfYooaGPgA1YN7RhTSAuDmUW5Hyfj5BH4NTtrzrvJxIhDoQLf31Fasjw00r4R0O" + } +] +` + fmt.Fprintln(w, gh_res) + })) + defer ts.Close() + + keys, err := fetchGithubKeys(ts.URL) + if err != nil { + t.Fatalf("Encountered unexpected error: %v", err) + } + expected := "ssh-dss AAAAB3NzaC1kc3MAAACBAIHAu822ggSkIHrJYvhmBceOSVjuflfQm8RbMMDNVe9relQfuPbN+nxGGTCKzPLebeOcX+Wwi77TPXWwK3BZMglfXxhABlFPsuMb63Tqp94pBYsJdx/iFj9iGo6pKoM1k8ubOcqsUnq+BR9895zRbE7MjdwkGo67+QhCEwvkwAnNAAAAFQCuddVqXLCubzqnWmeHLQE+2GFfHwAAAIBnlXW5h15ndVuwi0htF4oodVSB1KwnTWcuBK+aE1zRs76yvRb0Ws+oifumThDwB/Tec6FQuAfRKfy6piChZqsu5KvL98I+2t5yyi1td+kMvdTnVL2lW44etDKseOcozmknCOmh4Dqvhl/2MwrDAhlPaN08EEq9h3w3mXtNLWH64QAAAIBAzDOKr17llngaKIdDXh+LtXKh87+zfjlTA36/9r2uF2kYE5uApDtu9sPCkt7+YBQt7R8prADPckwAiXwVdk0xijIOpLDBmoydQJJRQ+zTMxvpQmUr/1kUOv0zb+lB657CgvN0vVTmP2swPeMvgntt3C4vw7Ab+O+MS9peOAJbbQ==" + if keys[0] != expected { + t.Fatalf("expected %s, got %s", expected, keys[0]) + } + expected = "ssh-dss AAAAB3NzaC1kc3MAAACBAPKRWdKhzGZuLAJL6M1eM51hWViMqNBC2C6lm2OqGRYLuIf1GJ391widUuSf4wQqnkR22Q9PCmAZ19XCf11wBRMnuw9I/Z3Bt5bXfc+dzFBCmHYGJ6wNSv++H9jxyMb+usmsenWOFZGNO2jN0wrJ4ay8Yt0bwtRU+VCXpuRLszMzAAAAFQDZUIuPjcfK5HLgnwZ/J3lvtvlUjQAAAIEApIkAwLuCQV5j3U6DmI/Y6oELqSUR2purFm8jo8jePFfe1t+ghikgD254/JXlhDCVgY0NLXcak+coJfGCTT23quJ7I5xdpTn/OZO2Q6Woum/bijFC/UWwQbLz0R2nU3DoHv5v6XHQZxuIG4Fsxa91S+vWjZFtI7RuYlBCZA//ANMAAACBAJO0FojzkX6IeaWLqrgu9GTkFwGFazZ+LPH5JOWPoPn1hQKuR32Uf6qNcBZcIjY7SF0P7HF5rLQd6zKZzHqqQQ92MV555NEwjsnJglYU8CaaZsfYooaGPgA1YN7RhTSAuDmUW5Hyfj5BH4NTtrzrvJxIhDoQLf31Fasjw00r4R0O" + if keys[2] != expected { + t.Fatalf("expected %s, got %s", expected, keys[2]) + } + +} +func TestCloudConfigUsersGithubUser(t *testing.T) { + + contents := []byte(` +users: + - name: elroy + coreos-ssh-import-github: bcwaldon +`) + cfg, err := NewCloudConfig(contents) + if err != nil { + t.Fatalf("Encountered unexpected error: %v", err) + } + + if len(cfg.Users) != 1 { + t.Fatalf("Parsed %d users, expected 1", cfg.Users) + } + + user := cfg.Users[0] + + if user.Name != "elroy" { + t.Errorf("User name is %q, expected 'elroy'", user.Name) + } + + if user.SSHImportGithubUser != "bcwaldon" { + t.Errorf("github user is %q, expected 'bcwaldon'", user.SSHImportGithubUser) + } +} diff --git a/system/user.go b/system/user.go index 1689bae..7b47b35 100644 --- a/system/user.go +++ b/system/user.go @@ -9,17 +9,18 @@ import ( ) type User struct { - Name string `yaml:"name"` - PasswordHash string `yaml:"passwd"` - SSHAuthorizedKeys []string `yaml:"ssh-authorized-keys"` - GECOS string `yaml:"gecos"` - Homedir string `yaml:"homedir"` - NoCreateHome bool `yaml:"no-create-home"` - PrimaryGroup string `yaml:"primary-group"` - Groups []string `yaml:"groups"` - NoUserGroup bool `yaml:"no-user-group"` - System bool `yaml:"system"` - NoLogInit bool `yaml:"no-log-init"` + Name string `yaml:"name"` + PasswordHash string `yaml:"passwd"` + SSHAuthorizedKeys []string `yaml:"ssh-authorized-keys"` + SSHImportGithubUser string `yaml:"coreos-ssh-import-github"` + GECOS string `yaml:"gecos"` + Homedir string `yaml:"homedir"` + NoCreateHome bool `yaml:"no-create-home"` + PrimaryGroup string `yaml:"primary-group"` + Groups []string `yaml:"groups"` + NoUserGroup bool `yaml:"no-user-group"` + System bool `yaml:"system"` + NoLogInit bool `yaml:"no-log-init"` } func UserExists(u *User) bool {