You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

260 lines
6.8 KiB

package realms
import (
"crypto/rsa"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
log "github.com/Sirupsen/logrus"
"github.com/andygrunwald/go-jira"
"github.com/dghubble/oauth1"
"github.com/matrix-org/go-neb/database"
"github.com/matrix-org/go-neb/realms/jira/urls"
"github.com/matrix-org/go-neb/types"
"golang.org/x/net/context"
"net/http"
)
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
PublicKeyPEM string // clobbered based on PrivateKeyPEM
PrivateKeyPEM string
}
type JIRASession struct {
id string // request token
userID string
realmID string
Secret string // request secret
}
func (s *JIRASession) UserID() string {
return s.userID
}
func (s *JIRASession) RealmID() string {
return s.realmID
}
func (s *JIRASession) ID() string {
return s.id
}
func (r *jiraRealm) ID() string {
return r.id
}
func (r *jiraRealm) Type() string {
return "jira"
}
func (r *jiraRealm) Register() error {
if r.ConsumerName == "" || r.ConsumerKey == "" || r.ConsumerSecret == "" || r.PrivateKeyPEM == "" {
return errors.New("ConsumerName, ConsumerKey, ConsumerSecret, PrivateKeyPEM must be specified.")
}
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
}
// 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
}
func (r *jiraRealm) RequestAuthSession(userID string, req json.RawMessage) interface{} {
logger := log.WithField("jira_url", r.JIRAEndpoint)
// Parse the private key as we may not have called Register()
err := r.parsePrivateKey()
if err != nil {
logger.WithError(err).Print("Failed to parse private key")
return nil
}
ju, err := urls.ParseJIRAURL(r.JIRAEndpoint)
if err != nil {
log.WithError(err).Print("Failed to parse JIRA endpoint")
return nil
}
authConfig := r.oauth1Config(ju)
reqToken, reqSec, err := authConfig.RequestToken()
if err != nil {
logger.WithError(err).Print("Failed to request auth token")
return nil
}
logger.WithField("req_token", reqToken).Print("Received request token")
authURL, err := authConfig.AuthorizationURL(reqToken)
if err != nil {
logger.WithError(err).Print("Failed to create authorization URL")
return nil
}
_, err = database.GetServiceDB().StoreAuthSession(&JIRASession{
id: reqToken,
userID: userID,
realmID: r.id,
Secret: reqSec,
})
if err != nil {
log.WithError(err).Print("Failed to store new auth session")
return nil
}
return &struct {
URL string
}{authURL.String()}
}
func (r *jiraRealm) OnReceiveRedirect(w http.ResponseWriter, req *http.Request) {
}
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 := r.oauth1Config(u)
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 {
return err
}
pub, err := publicKeyAsPEM(pk)
if err != nil {
return err
}
r.PublicKeyPEM = pub
r.privateKey = pk
return nil
}
func (r *jiraRealm) oauth1Config(u urls.JIRAURL) *oauth1.Config {
return &oauth1.Config{
ConsumerKey: r.ConsumerKey,
ConsumerSecret: r.ConsumerSecret,
// TODO: path from goneb.go - we should factor it out like we did with Services
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,
},
}
}
func loadPrivateKey(privKeyPEM string) (*rsa.PrivateKey, error) {
// Decode PEM to grab the private key type
block, _ := pem.Decode([]byte(privKeyPEM))
if block == nil {
return nil, errors.New("No PEM formatted block found")
}
priv, err := x509.ParsePKCS1PrivateKey(block.Bytes)
if err != nil {
return nil, err
}
return priv, nil
}
func publicKeyAsPEM(pkey *rsa.PrivateKey) (string, error) {
// https://github.com/golang-samples/cipher/blob/master/crypto/rsa_keypair.go
der, err := x509.MarshalPKIXPublicKey(&pkey.PublicKey)
if err != nil {
return "", err
}
block := pem.Block{
Type: "PUBLIC KEY",
Headers: nil,
Bytes: der,
}
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}
})
}