Browse Source

Merge pull request #16 from matrix-org/kegan/jira-auth

Allow JIRA auth realms to be configured
pull/17/head
Kegsay 8 years ago
committed by GitHub
parent
commit
a64427efdb
  1. 61
      README.md
  2. 94
      src/github.com/matrix-org/go-neb/realms/jira/jira.go
  3. 107
      src/github.com/matrix-org/go-neb/realms/jira/urls/urls.go
  4. 45
      src/github.com/matrix-org/go-neb/realms/jira/urls/urls_test.go

61
README.md

@ -145,6 +145,67 @@ curl -X POST localhost:4050/admin/configureService --data-binary '{
This request will make `BotUserID` join the `Rooms` specified and create webhooks for the `owner/repo` projects given.
## Starting a JIRA Service
### Register a JIRA realm
Generate an RSA private key: (JIRA does not support key sizes >2048 bits)
```bash
openssl genrsa -out privkey.pem 2048
cat privkey.pem
```
Create the realm:
```
curl -X POST localhost:4050/admin/configureAuthRealm --data-binary '{
"ID": "jirarealm",
"Type": "jira",
"Config": {
"JIRAEndpoint": "matrix.org/jira/",
"ConsumerName": "goneb",
"ConsumerKey": "goneb",
"ConsumerSecret": "random_long_string",
"PrivateKeyPEM": "-----BEGIN RSA PRIVATE KEY-----\r\nMIIEowIBAAKCAQEA39UhbOvQHEkBP9fGnhU+eSObTWBDGWygVYzbcONOlqEOTJUN\r\n8gmnellWqJO45S4jB1vLLnuXiHqEWnmaShIvbUem3QnDDqghu0gfqXHMlQr5R8ZP\r\norTt1F2idWy1wk5rVXeLKSG7uriYhDVOVS69WuefoW5v55b5YZV283v2jROjxHuj\r\ngAsJA7k6tvpYiSXApUl6YHmECfBoiwG9bwItkHwhZ\/fG9i4H8\/aOyr3WlaWbVeKX\r\n+m38lmYZvzQFRAk5ab1vzCGz4cyc\r\nTk2qmZpcjHRd1ijcOkgC23KF8lHWF5Zx0tySR+DWL1JeGm8NJxKMRJZuE8MIkJYF\r\nryE7kjspNItk6npkA3\/A4PWwElhddI4JpiuK+29mMNipRcYYy9e0vH\/igejv7ayd\r\nPLCRMQKBgBDSNWlZT0nNd2DXVqTW9p+MG72VKhDgmEwFB1acOw0lpu1XE8R1wmwG\r\nZRl\/xzri3LOW2Gpc77xu6fs3NIkzQw3v1ifYhX3OrVsCIRBbDjPQI3yYjkhGx24s\r\nVhhZ5S\/TkGk3Kw59bDC6KGqAuQAwX9req2l1NiuNaPU9rE7tf6Bk\r\n-----END RSA PRIVATE KEY-----"
}
}'
```
Returns:
```json
{
"ID": "jirarealm",
"Type": "jira",
"OldConfig": null,
"NewConfig": {
"JIRAEndpoint": "https://matrix.org/jira/",
"Server": "Matrix.org",
"Version": "6.3.5a",
"ConsumerName": "goneb",
"ConsumerKey": "goneb",
"ConsumerSecret": "random_long_string",
"PublicKeyPEM": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA39UhbOvQHEkBP9fGnhU+\neSObTWBDGWygVYzbcONOlqEOTJUN8gmnellWqJO45S4jB1vLLnuXiHqEWnmaShIv\nbUem3QnDDqghu0gfqXHMlQr5R8ZPorTt1F2idWy1wk5rVXeLKSG7uriYhDVOVS69\nWuefoW5v55b5YZV283v2jROjxHujgAsJA7k6tvpYiSXApUl6YHmECfBoiwG9bwIt\nkHwhZ/fG9i4H8/aOyr3WlaWbVeKX+m38lmYZvzQFRd7UPU7DuO6Aiqj7RxrbAvqq\ndPeoAvo6+V0TRPZ8YzKp2yQmDcGH69IbuKJ2BG1Qx8znZAvghKQ6P9Im+M4c7j9i\ndwIDAQAB\n-----END PUBLIC KEY-----\n",
"PrivateKeyPEM": "-----BEGIN RSA PRIVATE KEY-----\r\nMIIEowIBAAKCAQEA39UhbOvQHEkBP9fGnhU+eSObTWBDGWygVYzbcONOlqEOTJUN\r\n8gmnellWqJO45S4jB1vLLnuXiHqEWnmaShIvbUem3QnDDqghu0gfqXHMlQr5R8ZP\r\norTt1F2idWy1wk5rVXeLKSG7uriYhDVOVS69WuefoW5v55b5YZV283v2jROjxHuj\r\ngAsJA7k6tvpYiSXApUl6YHmECfBoiwG9bwItkHwhZ/fG9i4H8/aOyr3WlaWbVeKX\r\n+m38lmYZvzQFRd7UPU7DuO6Aiqj7RxrbAvqqdPeoAvo6+V0TRPZ8YzKp2yQmDcGH\r\n69IbuKJ2BG1Qx8znZAvghKQ6P9Im+M4c7j9iMG72VKhDgmEwFB1acOw0lpu1XE8R1wmwG\r\nZRl/xzri3LOW2Gpc77xu6fs3NIkzQw3v1ifYhX3OrVsCIRBbDjPQI3yYjkhGx24s\r\nVhhZ5S/TkGk3Kw59bDC6KGqAuQAwX9req2l1NiuNaPU9rE7tf6Bk\r\n-----END RSA PRIVATE KEY-----"
}
}
```
The `ConsumerKey`, `ConsumerSecret`, `ConsumerName` and `PublicKeyPEM` must be manually inserted
into the "Application Links" section under JIRA Admin Settings by a JIRA admin on the target
JIRA installation. Once that is complete, users can OAuth on the target JIRA installation.
### Make a request for JIRA Auth
TODO
### Create a JIRA bot
TODO
# Developing on go-neb.
There's a bunch more tools this project uses when developing in order to do

94
src/github.com/matrix-org/go-neb/realms/jira/jira.go

@ -7,7 +7,11 @@ import (
"encoding/pem"
"errors"
log "github.com/Sirupsen/logrus"
"github.com/andygrunwald/go-jira"
"github.com/dghubble/oauth1"
"github.com/matrix-org/go-neb/realms/jira/urls"
"github.com/matrix-org/go-neb/types"
"golang.org/x/net/context"
"net/http"
)
@ -15,6 +19,8 @@ type jiraRealm struct {
id string
privateKey *rsa.PrivateKey
JIRAEndpoint string
Server string // clobbered based on /serverInfo request
Version string // clobbered based on /serverInfo request
ConsumerName string
ConsumerKey string
ConsumerSecret string
@ -34,14 +40,39 @@ func (r *jiraRealm) Register() error {
if r.ConsumerName == "" || r.ConsumerKey == "" || r.ConsumerSecret == "" || r.PrivateKeyPEM == "" {
return errors.New("ConsumerName, ConsumerKey, ConsumerSecret, PrivateKeyPEM must be specified.")
}
log.Print("Registering..")
if r.JIRAEndpoint == "" {
return errors.New("JIRAEndpoint must be specified")
}
// Make sure the private key PEM is actually a private key.
err := r.parsePrivateKey()
if err != nil {
return err
}
// TODO: Check to see if JIRA endpoint is valid and known
// Parse the messy input URL into a canonicalised form.
ju, err := urls.ParseJIRAURL(r.JIRAEndpoint)
if err != nil {
return err
}
r.JIRAEndpoint = ju.Base
// Check to see if JIRA endpoint is valid by pinging an endpoint
cli, err := r.jiraClient(ju, "", true)
if err != nil {
return err
}
info, err := jiraServerInfo(cli)
if err != nil {
return err
}
log.WithFields(log.Fields{
"jira_url": ju.Base,
"title": info.ServerTitle,
"version": info.Version,
}).Print("Found JIRA endpoint")
r.Server = info.ServerTitle
r.Version = info.Version
return nil
}
@ -58,6 +89,47 @@ func (r *jiraRealm) AuthSession(id, userID, realmID string) types.AuthSession {
return nil
}
// jiraClient returns an authenticated jira.Client for the given userID. Returns an unauthenticated
// client if allowUnauth is true and no authenticated session is found, else returns an error.
func (r *jiraRealm) jiraClient(u urls.JIRAURL, userID string, allowUnauth bool) (*jira.Client, error) {
// TODO: Check if user has an auth session. Requires access token+secret
hasAuthSession := false
if hasAuthSession {
// make an authenticated client
var cli *jira.Client
auth := &oauth1.Config{
ConsumerKey: r.ConsumerKey,
ConsumerSecret: r.ConsumerSecret,
CallbackURL: u.Base + "realms/redirect/" + r.id,
// TODO: In JIRA Cloud, the Authorization URL is only the Instance BASE_URL:
// https://BASE_URL.atlassian.net.
// It also does not require the + "/plugins/servlet/oauth/authorize"
// We should probably check the provided JIRA base URL to see if it is a cloud one
// then adjust accordingly.
Endpoint: oauth1.Endpoint{
RequestTokenURL: u.Base + "plugins/servlet/oauth/request-token",
AuthorizeURL: u.Base + "plugins/servlet/oauth/authorize",
AccessTokenURL: u.Base + "plugins/servlet/oauth/access-token",
},
Signer: &oauth1.RSASigner{
PrivateKey: r.privateKey,
},
}
httpClient := auth.Client(context.TODO(), oauth1.NewToken("access_tokenTODO", "access_secretTODO"))
cli, err := jira.NewClient(httpClient, u.Base)
return cli, err
} else if allowUnauth {
// make an unauthenticated client
cli, err := jira.NewClient(nil, u.Base)
return cli, err
} else {
return nil, errors.New("No authenticated session found for " + userID)
}
}
func (r *jiraRealm) parsePrivateKey() error {
pk, err := loadPrivateKey(r.PrivateKeyPEM)
if err != nil {
@ -100,6 +172,24 @@ func publicKeyAsPEM(pkey *rsa.PrivateKey) (string, error) {
return string(pem.EncodeToMemory(&block)), nil
}
// jiraServiceInfo is the HTTP response to JIRA_ENDPOINT/rest/api/2/serverInfo
type jiraServiceInfo struct {
ServerTitle string `json:"serverTitle"`
Version string `json:"version"`
VersionNumbers []int `json:"versionNumbers"`
BaseURL string `json:"baseUrl"`
}
func jiraServerInfo(cli *jira.Client) (*jiraServiceInfo, error) {
var jsi jiraServiceInfo
req, _ := cli.NewRequest("GET", "rest/api/2/serverInfo", nil)
_, err := cli.Do(req, &jsi)
if err != nil {
return nil, err
}
return &jsi, nil
}
func init() {
types.RegisterAuthRealm(func(realmID string) types.AuthRealm {
return &jiraRealm{id: realmID}

107
src/github.com/matrix-org/go-neb/realms/jira/urls/urls.go

@ -0,0 +1,107 @@
// Package urls handles converting between various JIRA URL representations in a consistent way. There exists three main
// types of JIRA URL which Go-NEB cares about:
// - URL Keys => matrix.org/jira
// - Base URLs => https://matrix.org/jira/
// - REST URLs => https://matrix.org/jira/rest/api/2/issue/12680
// When making outbound requests to JIRA, Go-NEB needs to use the Base URL representation. Likewise, when Go-NEB
// sends Matrix messages with JIRA URLs in them, the Base URL needs to be used to form the URL. The URL Key is
// used to determine equivalence of various JIRA installations and is mainly required when searching the database.
// The REST URLs are present on incoming webhook events and are the only way to map the event to a JIRA installation.
package urls
import (
"errors"
"net/url"
"strings"
)
// JIRAURL contains the parsed representation of a JIRA URL
type JIRAURL struct {
Base string // The base URL of the JIRA installation. Always has a trailing / and a protocol.
Key string // The URL key of the JIRA installation. Never has a trailing / or a protocol.
Raw string // The raw input URL, if given. Freeform.
}
// ParseJIRAURL parses a raw input URL and returns a struct which has various JIRA URL representations. The input
// URL can be a JIRA REST URL, a speculative base JIRA URL from a client, or a URL key. The input string will be
// stored as under JIRAURL.Raw. If a URL key is given, this struct will default to https as the protocol.
func ParseJIRAURL(u string) (j JIRAURL, err error) {
if u == "" {
err = errors.New("No input JIRA URL")
return
}
j.Raw = u
// URL keys don't have a protocol, everything else does
if !strings.HasPrefix(u, "https://") && !strings.HasPrefix(u, "http://") {
// assume input is a URL key
k, e := makeURLKey(u)
if e != nil {
err = e
return
}
j.Key = k
j.Base = makeBaseURL(u)
return
}
// Attempt to parse out REST API paths. This is a horrible heuristic which mostly works.
if strings.Contains(u, "/rest/api/") {
j.Base = makeBaseURL(strings.Split(u, "/rest/api/")[0])
} else {
// Assume it already is a base URL
j.Base = makeBaseURL(u)
}
k, e := makeURLKey(j.Base)
if e != nil {
err = e
return
}
j.Key = k
return
}
// SameJIRAURL returns true if the two given JIRA URLs are pointing to the same JIRA installation.
// Equivalence is determined solely by the provided URLs, by sanitising them then comparing.
func SameJIRAURL(a, b string) bool {
ja, err := ParseJIRAURL(a)
if err != nil {
return false
}
jb, err := ParseJIRAURL(b)
if err != nil {
return false
}
return ja.Key == jb.Key
}
// makeBaseURL assumes the input is a base URL and makes sure that the string conforms to JIRA Base URL rules:
// - Must have a protocol
// - Must have a trailing slash
// Defaults to HTTPS if there is no protocol specified.
func makeBaseURL(s string) string {
if !strings.HasPrefix(s, "https://") && !strings.HasPrefix(s, "http://") {
s = "https://" + s
}
return withTrailingSlash(s)
}
// makeURLKey assumes the input is a URL key and makes sure that the string conforms to JIRA URL Key rules:
// - Must not have a protocol
// - Must not have a trailing slash
// For example:
// https://matrix.org/jira/ => matrix.org/jira
func makeURLKey(s string) (string, error) {
u, err := url.Parse(s)
if err != nil {
return "", err
}
return u.Host + strings.TrimSuffix(u.Path, "/"), nil
}
// withTrailingSlash makes sure the input string has a trailing slash. Will not add one if one already exists.
func withTrailingSlash(s string) string {
if strings.HasSuffix(s, "/") {
return s
}
return s + "/"
}

45
src/github.com/matrix-org/go-neb/realms/jira/urls/urls_test.go

@ -0,0 +1,45 @@
package urls
import (
"testing"
)
var urltests = []struct {
in string
outBase string
outKey string
outRaw string
}{
// valid url key as input
{"matrix.org/jira", "https://matrix.org/jira/", "matrix.org/jira", "matrix.org/jira"},
// valid url base as input
{"https://matrix.org/jira/", "https://matrix.org/jira/", "matrix.org/jira", "https://matrix.org/jira/"},
// valid rest url as input
{"https://matrix.org/jira/rest/api/2/issue/12680", "https://matrix.org/jira/", "matrix.org/jira", "https://matrix.org/jira/rest/api/2/issue/12680"},
// missing trailing slash as input
{"https://matrix.org/jira", "https://matrix.org/jira/", "matrix.org/jira", "https://matrix.org/jira"},
// missing protocol but with trailing slash
{"matrix.org/jira/", "https://matrix.org/jira/", "matrix.org/jira", "matrix.org/jira/"},
// no jira path as base url (subdomain)
{"https://jira.matrix.org", "https://jira.matrix.org/", "jira.matrix.org", "https://jira.matrix.org"},
// explicit http as input
{"http://matrix.org/jira", "http://matrix.org/jira/", "matrix.org/jira", "http://matrix.org/jira"},
}
func TestParseJIRAURL(t *testing.T) {
for _, urltest := range urltests {
jURL, err := ParseJIRAURL(urltest.in)
if err != nil {
t.Fatal(err)
}
if jURL.Key != urltest.outKey {
t.Fatalf("ParseJIRAURL(%s) => Key: Want %s got %s", urltest.in, urltest.outKey, jURL.Key)
}
if jURL.Base != urltest.outBase {
t.Fatalf("ParseJIRAURL(%s) => Base: Want %s got %s", urltest.in, urltest.outBase, jURL.Base)
}
if jURL.Raw != urltest.outRaw {
t.Fatalf("ParseJIRAURL(%s) => Raw: Want %s got %s", urltest.in, urltest.outRaw, jURL.Raw)
}
}
}
Loading…
Cancel
Save