From 12a75dd304d2aebfba4107f3cda0641a186f38a1 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 1 Apr 2015 15:46:21 -0700 Subject: [PATCH] credential/github: auth with github --- builtin/credential/github/backend.go | 66 ++++++++++++++++ builtin/credential/github/backend_test.go | 63 +++++++++++++++ builtin/credential/github/path_config.go | 61 +++++++++++++++ builtin/credential/github/path_login.go | 95 +++++++++++++++++++++++ 4 files changed, 285 insertions(+) create mode 100644 builtin/credential/github/backend.go create mode 100644 builtin/credential/github/backend_test.go create mode 100644 builtin/credential/github/path_config.go create mode 100644 builtin/credential/github/path_login.go diff --git a/builtin/credential/github/backend.go b/builtin/credential/github/backend.go new file mode 100644 index 000000000..d5c6fefef --- /dev/null +++ b/builtin/credential/github/backend.go @@ -0,0 +1,66 @@ +package github + +import ( + "net/http" + + "github.com/google/go-github/github" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" + "golang.org/x/oauth2" +) + +func Factory(map[string]string) (logical.Backend, error) { + return Backend(), nil +} + +func Backend() *framework.Backend { + var b backend + b.Map = &framework.PolicyMap{ + PathMap: &framework.PathMap{"teams"}, + DefaultKey: "default", + } + b.Backend = &framework.Backend{ + PathsSpecial: &logical.Paths{ + Root: []string{ + "config", + }, + + Unauthenticated: []string{ + "login", + }, + }, + + Paths: append([]*framework.Path{ + pathConfig(), + pathLogin(&b), + }, b.Map.Paths()...), + } + + return b.Backend +} + +type backend struct { + *framework.Backend + + Map *framework.PolicyMap +} + +// Client returns the GitHub client to communicate to GitHub via the +// configured settings. +func (b *backend) Client(token string) (*github.Client, error) { + var tc *http.Client + if token != "" { + tc = oauth2.NewClient(oauth2.NoContext, &tokenSource{Value: token}) + } + + return github.NewClient(tc), nil +} + +// tokenSource is an oauth2.TokenSource implementation. +type tokenSource struct { + Value string +} + +func (t *tokenSource) Token() (*oauth2.Token, error) { + return &oauth2.Token{AccessToken: t.Value}, nil +} diff --git a/builtin/credential/github/backend_test.go b/builtin/credential/github/backend_test.go new file mode 100644 index 000000000..cfcd5f30e --- /dev/null +++ b/builtin/credential/github/backend_test.go @@ -0,0 +1,63 @@ +package github + +import ( + "os" + "testing" + + "github.com/hashicorp/vault/logical" + logicaltest "github.com/hashicorp/vault/logical/testing" +) + +func TestBackend_basic(t *testing.T) { + logicaltest.Test(t, logicaltest.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Backend: Backend(), + Steps: []logicaltest.TestStep{ + testAccStepConfig(t), + testAccMap(t), + testAccLogin(t), + }, + }) +} + +func testAccPreCheck(t *testing.T) { + if v := os.Getenv("GITHUB_TOKEN"); v == "" { + t.Fatal("GITHUB_USER must be set for acceptance tests") + } + + if v := os.Getenv("GITHUB_ORG"); v == "" { + t.Fatal("GITHUB_ORG must be set for acceptance tests") + } +} + +func testAccStepConfig(t *testing.T) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.WriteOperation, + Path: "config", + Data: map[string]interface{}{ + "organization": os.Getenv("GITHUB_ORG"), + }, + } +} + +func testAccMap(t *testing.T) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.WriteOperation, + Path: "map/teams/default", + Data: map[string]interface{}{ + "value": "foo", + }, + } +} +func testAccLogin(t *testing.T) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.WriteOperation, + Path: "login", + Data: map[string]interface{}{ + "token": os.Getenv("GITHUB_TOKEN"), + }, + Unauthenticated: true, + + Check: logicaltest.TestCheckAuth([]string{"foo"}), + } +} diff --git a/builtin/credential/github/path_config.go b/builtin/credential/github/path_config.go new file mode 100644 index 000000000..0c6db566a --- /dev/null +++ b/builtin/credential/github/path_config.go @@ -0,0 +1,61 @@ +package github + +import ( + "fmt" + + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathConfig() *framework.Path { + return &framework.Path{ + Pattern: "config", + Fields: map[string]*framework.FieldSchema{ + "organization": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "The organization users must be part of", + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.WriteOperation: pathConfigWrite, + }, + } +} + +func pathConfigWrite( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + entry, err := logical.StorageEntryJSON("config", config{ + Org: data.Get("organization").(string), + }) + if err != nil { + return nil, err + } + + if err := req.Storage.Put(entry); err != nil { + return nil, err + } + + return nil, nil +} + +// Config returns the configuration for this backend. +func (b *backend) Config(s logical.Storage) (*config, error) { + entry, err := s.Get("config") + if err != nil { + return nil, err + } + + var result config + if entry != nil { + if err := entry.DecodeJSON(&result); err != nil { + return nil, fmt.Errorf("error reading configuration: %s", err) + } + } + + return &result, nil +} + +type config struct { + Org string `json:"organization"` +} diff --git a/builtin/credential/github/path_login.go b/builtin/credential/github/path_login.go new file mode 100644 index 000000000..f173de9fe --- /dev/null +++ b/builtin/credential/github/path_login.go @@ -0,0 +1,95 @@ +package github + +import ( + "github.com/google/go-github/github" + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" +) + +func pathLogin(b *backend) *framework.Path { + return &framework.Path{ + Pattern: "login", + Fields: map[string]*framework.FieldSchema{ + "token": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "GitHub personal API token", + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.WriteOperation: b.pathLogin, + }, + } +} + +func (b *backend) pathLogin( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + // Get all our stored state + config, err := b.Config(req.Storage) + if err != nil { + return nil, err + } + if config.Org == "" { + return logical.ErrorResponse( + "configure the github credential backend first"), nil + } + + client, err := b.Client(data.Get("token").(string)) + if err != nil { + return nil, err + } + + // Get the user + user, _, err := client.Users.Get("") + if err != nil { + return nil, err + } + + // Verify that the user is part of the organization + var org *github.Organization + orgs, _, err := client.Organizations.List("", nil) + if err != nil { + return nil, err + } + + for _, o := range orgs { + if *o.Login == config.Org { + org = &o + break + } + } + if org == nil { + return logical.ErrorResponse("user is not part of required org"), nil + } + + // Get the teams that this user is part of to determine the policies + var teamNames []string + teams, _, err := client.Organizations.ListUserTeams(nil) + if err != nil { + return nil, err + } + for _, t := range teams { + // We only care about teams that are part of the organization we use + if *t.Organization.ID != *org.ID { + continue + } + + // Append the names so we can get the policies + teamNames = append(teamNames, *t.Name) + } + + policiesList, err := b.Map.Policies(req.Storage, teamNames...) + if err != nil { + return nil, err + } + + return &logical.Response{ + Auth: &logical.Auth{ + Policies: policiesList, + Metadata: map[string]string{ + "username": *user.Login, + "org": *org.Login, + }, + }, + }, nil +}