Browse Source

Merge pull request #11 from matrix-org/kegan/github-service

Make GithubService construct authenticated github clients
pull/12/head
Kegsay 8 years ago
committed by GitHub
parent
commit
97e2ace55d
  1. 5
      src/github.com/matrix-org/go-neb/api.go
  2. 4
      src/github.com/matrix-org/go-neb/plugin/plugin.go
  3. 28
      src/github.com/matrix-org/go-neb/plugin/plugin_test.go
  4. 20
      src/github.com/matrix-org/go-neb/realms/github/github.go
  5. 1
      src/github.com/matrix-org/go-neb/services/echo/echo.go
  6. 88
      src/github.com/matrix-org/go-neb/services/github/github.go
  7. 1
      src/github.com/matrix-org/go-neb/types/types.go

5
src/github.com/matrix-org/go-neb/api.go

@ -198,6 +198,11 @@ func (s *configureServiceHandler) OnIncomingRequest(req *http.Request) (interfac
return nil, &errors.HTTPError{err, "Error parsing config JSON", 400}
}
err := service.Register()
if err != nil {
return nil, &errors.HTTPError{err, "Failed to register service: " + err.Error(), 500}
}
client, err := s.clients.Client(service.ServiceUserID())
if err != nil {
return nil, &errors.HTTPError{err, "Unknown matrix client", 400}

4
src/github.com/matrix-org/go-neb/plugin/plugin.go

@ -31,7 +31,7 @@ type Command struct {
// the appropriate RFC.
type Expansion struct {
Regexp *regexp.Regexp
Expand func(roomID, matchingText string) interface{}
Expand func(roomID, userID, matchingText string) interface{}
}
// matches if the arguments start with the path of the command.
@ -95,7 +95,7 @@ func runExpansionsForPlugin(plugin Plugin, event *matrix.Event, body string) []i
continue
}
matches[matchingText] = true
if response := expansion.Expand(event.RoomID, matchingText); response != nil {
if response := expansion.Expand(event.RoomID, event.Sender, matchingText); response != nil {
responses = append(responses, response)
}
}

28
src/github.com/matrix-org/go-neb/plugin/plugin_test.go

@ -26,21 +26,21 @@ func makeTestEvent(msgtype, body string) *matrix.Event {
type testResponse struct {
RoomID string
Sender string
Arguments []string
}
func makeTestResponse(roomID, sender string, arguments []string) interface{} {
return testResponse{roomID, sender, arguments}
return testResponse{roomID, arguments}
}
type testExpansion struct {
RoomID string
UserID string
MatchingText string
}
func makeTestExpansion(roomID, matchingText string) interface{} {
return testExpansion{roomID, matchingText}
func makeTestExpansion(roomID, userID, matchingText string) interface{} {
return testExpansion{roomID, userID, matchingText}
}
func makeTestPlugin(paths [][]string, regexps []*regexp.Regexp) Plugin {
@ -74,7 +74,7 @@ func TestRunCommands(t *testing.T) {
"arg1", "arg 2", "arg 3",
})}
if !reflect.DeepEqual(got, want) {
t.Errorf("runCommands(%q, %q) == %q, want %q", plugins, event, got, want)
t.Errorf("runCommands(\nplugins=%+v\nevent=%+v\n)\n%+v\nwanted: %+v", plugins, event, got, want)
}
}
@ -89,7 +89,7 @@ func TestRunCommandsBestMatch(t *testing.T) {
"arg1",
})}
if !reflect.DeepEqual(got, want) {
t.Errorf("runCommands(%q, %q) == %q, want %q", plugins, event, got, want)
t.Errorf("runCommands(\nplugins=%+v\nevent=%+v\n)\n%+v\nwanted: %+v", plugins, event, got, want)
}
}
@ -105,7 +105,7 @@ func TestRunCommandsMultiplePlugins(t *testing.T) {
makeTestResponse(myRoomID, mySender, []string{"first", "arg1"}),
}
if !reflect.DeepEqual(got, want) {
t.Errorf("runCommands(%q, %q) == %q, want %q", plugins, event, got, want)
t.Errorf("runCommands(\nplugins=%+v\nevent=%+v\n)\n%+v\nwanted: %+v", plugins, event, got, want)
}
}
@ -119,7 +119,7 @@ func TestRunCommandsInvalidShell(t *testing.T) {
makeTestResponse(myRoomID, mySender, []string{"'mismatched", `quotes"`}),
}
if !reflect.DeepEqual(got, want) {
t.Errorf("runCommands(%q, %q) == %q, want %q", plugins, event, got, want)
t.Errorf("runCommands(\nplugins=%+v\nevent=%+v\n)\n%+v\nwanted: %+v", plugins, event, got, want)
}
}
@ -133,12 +133,12 @@ func TestExpansion(t *testing.T) {
event := makeTestEvent("m.text", "test banana for scale")
got := runCommands(plugins, event)
want := []interface{}{
makeTestExpansion(myRoomID, "anana"),
makeTestExpansion(myRoomID, "ale"),
makeTestExpansion(myRoomID, "ban"),
makeTestExpansion(myRoomID, mySender, "anana"),
makeTestExpansion(myRoomID, mySender, "ale"),
makeTestExpansion(myRoomID, mySender, "ban"),
}
if !reflect.DeepEqual(got, want) {
t.Errorf("runCommands(%q, %q) == %q, want %q", plugins, event, got, want)
t.Errorf("runCommands(\nplugins=%+v\nevent=%+v\n)\n%+v\nwanted: %+v", plugins, event, got, want)
}
}
@ -151,9 +151,9 @@ func TestExpansionDuplicateMatches(t *testing.T) {
event := makeTestEvent("m.text", "badger badger badger")
got := runCommands(plugins, event)
want := []interface{}{
makeTestExpansion(myRoomID, "badger"),
makeTestExpansion(myRoomID, mySender, "badger"),
}
if !reflect.DeepEqual(got, want) {
t.Errorf("runCommands(%q, %q) == %q, want %q", plugins, event, got, want)
t.Errorf("runCommands(\nplugins=%+v\nevent=%+v\n)\n%+v\nwanted: %+v", plugins, event, got, want)
}
}

20
src/github.com/matrix-org/go-neb/realms/github/github.go

@ -19,23 +19,29 @@ type githubRealm struct {
RedirectBaseURI string
}
type githubSession struct {
// GithubSession represents an authenticated github session
type GithubSession struct {
// AccessToken is the github access token for the user
AccessToken string
// Scopes are the set of *ALLOWED* scopes (which may not be the same as the requested scopes)
Scopes string
id string
userID string
realmID string
}
func (s *githubSession) UserID() string {
// UserID returns the user_id who authorised with Github
func (s *GithubSession) UserID() string {
return s.userID
}
func (s *githubSession) RealmID() string {
// RealmID returns the realm ID of the realm which performed the authentication
func (s *GithubSession) RealmID() string {
return s.realmID
}
func (s *githubSession) ID() string {
// ID returns the session ID
func (s *GithubSession) ID() string {
return s.id
}
@ -61,7 +67,7 @@ func (r *githubRealm) RequestAuthSession(userID string, req json.RawMessage) int
// TODO: Path is from goneb.go - we should probably factor it out.
q.Set("redirect_uri", r.RedirectBaseURI+"/realms/redirects/"+r.ID())
u.RawQuery = q.Encode()
session := &githubSession{
session := &GithubSession{
id: state, // key off the state for redirects
userID: userID,
realmID: r.ID(),
@ -96,7 +102,7 @@ func (r *githubRealm) OnReceiveRedirect(w http.ResponseWriter, req *http.Request
failWith(logger, w, 400, "Provided ?state= param is not recognised.", err)
return
}
ghSession, ok := session.(*githubSession)
ghSession, ok := session.(*GithubSession)
if !ok {
failWith(logger, w, 500, "Unexpected session found.", nil)
return
@ -136,7 +142,7 @@ func (r *githubRealm) OnReceiveRedirect(w http.ResponseWriter, req *http.Request
}
func (r *githubRealm) AuthSession(id, userID, realmID string) types.AuthSession {
return &githubSession{
return &GithubSession{
id: id,
userID: userID,
realmID: realmID,

1
src/github.com/matrix-org/go-neb/services/echo/echo.go

@ -18,6 +18,7 @@ func (e *echoService) ServiceUserID() string { return e.UserID }
func (e *echoService) ServiceID() string { return e.id }
func (e *echoService) ServiceType() string { return "echo" }
func (e *echoService) RoomIDs() []string { return e.Rooms }
func (e *echoService) Register() error { return nil }
func (e *echoService) Plugin(roomID string) plugin.Plugin {
return plugin.Plugin{
Commands: []plugin.Command{

88
src/github.com/matrix-org/go-neb/services/github/github.go

@ -4,14 +4,17 @@ import (
"fmt"
log "github.com/Sirupsen/logrus"
"github.com/google/go-github/github"
"github.com/matrix-org/go-neb/database"
"github.com/matrix-org/go-neb/matrix"
"github.com/matrix-org/go-neb/plugin"
"github.com/matrix-org/go-neb/realms/github"
"github.com/matrix-org/go-neb/services/github/webhook"
"github.com/matrix-org/go-neb/types"
"golang.org/x/oauth2"
"net/http"
"regexp"
"strconv"
"strings"
)
// Matches alphanumeric then a /, then more alphanumeric then a #, then a number.
@ -20,28 +23,43 @@ var ownerRepoIssueRegex = regexp.MustCompile("([A-z0-9-_]+)/([A-z0-9-_]+)#([0-9]
type githubService struct {
id string
UserID string
Rooms map[string][]string // room_id => ["push","issue","pull_request"]
BotUserID string
GithubUserID string
RealmID string
WebhookRooms map[string][]string // room_id => ["push","issue","pull_request"]
}
func (s *githubService) ServiceUserID() string { return s.UserID }
func (s *githubService) ServiceUserID() string { return s.BotUserID }
func (s *githubService) ServiceID() string { return s.id }
func (s *githubService) ServiceType() string { return "github" }
func (s *githubService) RoomIDs() []string {
var keys []string
for k := range s.Rooms {
for k := range s.WebhookRooms {
keys = append(keys, k)
}
return keys
}
func (s *githubService) Plugin(roomID string) plugin.Plugin {
return plugin.Plugin{
Commands: []plugin.Command{},
Commands: []plugin.Command{
plugin.Command{
Path: []string{"github", "create"},
Command: func(roomID, userID string, args []string) (interface{}, error) {
cli := s.githubClientFor(userID, false)
if cli == nil {
// TODO: send starter link
return &matrix.TextMessage{"m.notice",
userID + " : You have not linked your Github account."}, nil
}
return &matrix.TextMessage{"m.notice", strings.Join(args, " ")}, nil
},
},
},
Expansions: []plugin.Expansion{
plugin.Expansion{
Regexp: ownerRepoIssueRegex,
Expand: func(roomID, matchingText string) interface{} {
cli := githubClient("")
Expand: func(roomID, userID, matchingText string) interface{} {
cli := s.githubClientFor(userID, true)
owner, repo, num, err := ownerRepoNumberFromText(matchingText)
if err != nil {
log.WithError(err).WithField("text", matchingText).Print(
@ -75,7 +93,7 @@ func (s *githubService) OnReceiveWebhook(w http.ResponseWriter, req *http.Reques
return
}
for roomID, notif := range s.Rooms {
for roomID, notif := range s.WebhookRooms {
notifyRoom := false
for _, notifyType := range notif {
if evType == notifyType {
@ -99,6 +117,60 @@ func (s *githubService) OnReceiveWebhook(w http.ResponseWriter, req *http.Reques
}
w.WriteHeader(200)
}
func (s *githubService) Register() error {
if s.RealmID == "" || s.BotUserID == "" {
return fmt.Errorf("RealmID and BotUserID are required")
}
// check realm exists
realm, err := database.GetServiceDB().LoadAuthRealm(s.RealmID)
if err != nil {
return err
}
// make sure the realm is of the type we expect
if realm.Type() != "github" {
return fmt.Errorf("Realm is of type '%s', not 'github'", realm.Type())
}
return nil
}
func (s *githubService) githubClientFor(userID string, allowUnauth bool) *github.Client {
token, err := getTokenForUser(s.RealmID, userID)
if err != nil {
log.WithFields(log.Fields{
log.ErrorKey: err,
"user_id": userID,
"realm_id": s.RealmID,
}).Print("Failed to get token for user")
}
if token != "" {
return githubClient(token)
} else if allowUnauth {
return githubClient("")
} else {
return nil
}
}
func getTokenForUser(realmID, userID string) (string, error) {
realm, err := database.GetServiceDB().LoadAuthRealm(realmID)
if err != nil {
return "", err
}
if realm.Type() != "github" {
return "", fmt.Errorf("Bad realm type: %s", realm.Type())
}
// pull out the token (TODO: should the service know how the realm stores this?)
session, err := database.GetServiceDB().LoadAuthSessionByUser(realm.ID(), userID)
if err != nil {
return "", err
}
ghSession, ok := session.(*realms.GithubSession)
if !ok {
return "", fmt.Errorf("Session is not a github session: %s", session.ID())
}
return ghSession.AccessToken, nil
}
// githubClient returns a github Client which can perform Github API operations.
// If `token` is empty, a non-authenticated client will be created. This should be

1
src/github.com/matrix-org/go-neb/types/types.go

@ -35,6 +35,7 @@ type Service interface {
RoomIDs() []string
Plugin(roomID string) plugin.Plugin
OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli *matrix.Client)
Register() error
}
var servicesByType = map[string]func(string) Service{}

Loading…
Cancel
Save