credential/github: auth with github
This commit is contained in:
parent
04cf4ef093
commit
12a75dd304
|
@ -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
|
||||
}
|
|
@ -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"}),
|
||||
}
|
||||
}
|
|
@ -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"`
|
||||
}
|
|
@ -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
|
||||
}
|
Loading…
Reference in New Issue