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

  1. package realms
  2. import (
  3. "crypto/rsa"
  4. "crypto/x509"
  5. "encoding/json"
  6. "encoding/pem"
  7. "errors"
  8. log "github.com/Sirupsen/logrus"
  9. "github.com/andygrunwald/go-jira"
  10. "github.com/dghubble/oauth1"
  11. "github.com/matrix-org/go-neb/database"
  12. "github.com/matrix-org/go-neb/realms/jira/urls"
  13. "github.com/matrix-org/go-neb/types"
  14. "golang.org/x/net/context"
  15. "net/http"
  16. )
  17. type jiraRealm struct {
  18. id string
  19. privateKey *rsa.PrivateKey
  20. JIRAEndpoint string
  21. Server string // clobbered based on /serverInfo request
  22. Version string // clobbered based on /serverInfo request
  23. ConsumerName string
  24. ConsumerKey string
  25. ConsumerSecret string
  26. PublicKeyPEM string // clobbered based on PrivateKeyPEM
  27. PrivateKeyPEM string
  28. }
  29. type JIRASession struct {
  30. id string // request token
  31. userID string
  32. realmID string
  33. Secret string // request secret
  34. }
  35. func (s *JIRASession) UserID() string {
  36. return s.userID
  37. }
  38. func (s *JIRASession) RealmID() string {
  39. return s.realmID
  40. }
  41. func (s *JIRASession) ID() string {
  42. return s.id
  43. }
  44. func (r *jiraRealm) ID() string {
  45. return r.id
  46. }
  47. func (r *jiraRealm) Type() string {
  48. return "jira"
  49. }
  50. func (r *jiraRealm) Register() error {
  51. if r.ConsumerName == "" || r.ConsumerKey == "" || r.ConsumerSecret == "" || r.PrivateKeyPEM == "" {
  52. return errors.New("ConsumerName, ConsumerKey, ConsumerSecret, PrivateKeyPEM must be specified.")
  53. }
  54. if r.JIRAEndpoint == "" {
  55. return errors.New("JIRAEndpoint must be specified")
  56. }
  57. // Make sure the private key PEM is actually a private key.
  58. err := r.parsePrivateKey()
  59. if err != nil {
  60. return err
  61. }
  62. // Parse the messy input URL into a canonicalised form.
  63. ju, err := urls.ParseJIRAURL(r.JIRAEndpoint)
  64. if err != nil {
  65. return err
  66. }
  67. r.JIRAEndpoint = ju.Base
  68. // Check to see if JIRA endpoint is valid by pinging an endpoint
  69. cli, err := r.jiraClient(ju, "", true)
  70. if err != nil {
  71. return err
  72. }
  73. info, err := jiraServerInfo(cli)
  74. if err != nil {
  75. return err
  76. }
  77. log.WithFields(log.Fields{
  78. "jira_url": ju.Base,
  79. "title": info.ServerTitle,
  80. "version": info.Version,
  81. }).Print("Found JIRA endpoint")
  82. r.Server = info.ServerTitle
  83. r.Version = info.Version
  84. return nil
  85. }
  86. func (r *jiraRealm) RequestAuthSession(userID string, req json.RawMessage) interface{} {
  87. logger := log.WithField("jira_url", r.JIRAEndpoint)
  88. // Parse the private key as we may not have called Register()
  89. err := r.parsePrivateKey()
  90. if err != nil {
  91. logger.WithError(err).Print("Failed to parse private key")
  92. return nil
  93. }
  94. ju, err := urls.ParseJIRAURL(r.JIRAEndpoint)
  95. if err != nil {
  96. log.WithError(err).Print("Failed to parse JIRA endpoint")
  97. return nil
  98. }
  99. authConfig := r.oauth1Config(ju)
  100. reqToken, reqSec, err := authConfig.RequestToken()
  101. if err != nil {
  102. logger.WithError(err).Print("Failed to request auth token")
  103. return nil
  104. }
  105. logger.WithField("req_token", reqToken).Print("Received request token")
  106. authURL, err := authConfig.AuthorizationURL(reqToken)
  107. if err != nil {
  108. logger.WithError(err).Print("Failed to create authorization URL")
  109. return nil
  110. }
  111. _, err = database.GetServiceDB().StoreAuthSession(&JIRASession{
  112. id: reqToken,
  113. userID: userID,
  114. realmID: r.id,
  115. Secret: reqSec,
  116. })
  117. if err != nil {
  118. log.WithError(err).Print("Failed to store new auth session")
  119. return nil
  120. }
  121. return &struct {
  122. URL string
  123. }{authURL.String()}
  124. }
  125. func (r *jiraRealm) OnReceiveRedirect(w http.ResponseWriter, req *http.Request) {
  126. }
  127. func (r *jiraRealm) AuthSession(id, userID, realmID string) types.AuthSession {
  128. return nil
  129. }
  130. // jiraClient returns an authenticated jira.Client for the given userID. Returns an unauthenticated
  131. // client if allowUnauth is true and no authenticated session is found, else returns an error.
  132. func (r *jiraRealm) jiraClient(u urls.JIRAURL, userID string, allowUnauth bool) (*jira.Client, error) {
  133. // TODO: Check if user has an auth session. Requires access token+secret
  134. hasAuthSession := false
  135. if hasAuthSession {
  136. // make an authenticated client
  137. var cli *jira.Client
  138. auth := r.oauth1Config(u)
  139. httpClient := auth.Client(context.TODO(), oauth1.NewToken("access_tokenTODO", "access_secretTODO"))
  140. cli, err := jira.NewClient(httpClient, u.Base)
  141. return cli, err
  142. } else if allowUnauth {
  143. // make an unauthenticated client
  144. cli, err := jira.NewClient(nil, u.Base)
  145. return cli, err
  146. } else {
  147. return nil, errors.New("No authenticated session found for " + userID)
  148. }
  149. }
  150. func (r *jiraRealm) parsePrivateKey() error {
  151. pk, err := loadPrivateKey(r.PrivateKeyPEM)
  152. if err != nil {
  153. return err
  154. }
  155. pub, err := publicKeyAsPEM(pk)
  156. if err != nil {
  157. return err
  158. }
  159. r.PublicKeyPEM = pub
  160. r.privateKey = pk
  161. return nil
  162. }
  163. func (r *jiraRealm) oauth1Config(u urls.JIRAURL) *oauth1.Config {
  164. return &oauth1.Config{
  165. ConsumerKey: r.ConsumerKey,
  166. ConsumerSecret: r.ConsumerSecret,
  167. // TODO: path from goneb.go - we should factor it out like we did with Services
  168. CallbackURL: u.Base + "realms/redirect/" + r.id,
  169. // TODO: In JIRA Cloud, the Authorization URL is only the Instance BASE_URL:
  170. // https://BASE_URL.atlassian.net.
  171. // It also does not require the + "/plugins/servlet/oauth/authorize"
  172. // We should probably check the provided JIRA base URL to see if it is a cloud one
  173. // then adjust accordingly.
  174. Endpoint: oauth1.Endpoint{
  175. RequestTokenURL: u.Base + "plugins/servlet/oauth/request-token",
  176. AuthorizeURL: u.Base + "plugins/servlet/oauth/authorize",
  177. AccessTokenURL: u.Base + "plugins/servlet/oauth/access-token",
  178. },
  179. Signer: &oauth1.RSASigner{
  180. PrivateKey: r.privateKey,
  181. },
  182. }
  183. }
  184. func loadPrivateKey(privKeyPEM string) (*rsa.PrivateKey, error) {
  185. // Decode PEM to grab the private key type
  186. block, _ := pem.Decode([]byte(privKeyPEM))
  187. if block == nil {
  188. return nil, errors.New("No PEM formatted block found")
  189. }
  190. priv, err := x509.ParsePKCS1PrivateKey(block.Bytes)
  191. if err != nil {
  192. return nil, err
  193. }
  194. return priv, nil
  195. }
  196. func publicKeyAsPEM(pkey *rsa.PrivateKey) (string, error) {
  197. // https://github.com/golang-samples/cipher/blob/master/crypto/rsa_keypair.go
  198. der, err := x509.MarshalPKIXPublicKey(&pkey.PublicKey)
  199. if err != nil {
  200. return "", err
  201. }
  202. block := pem.Block{
  203. Type: "PUBLIC KEY",
  204. Headers: nil,
  205. Bytes: der,
  206. }
  207. return string(pem.EncodeToMemory(&block)), nil
  208. }
  209. // jiraServiceInfo is the HTTP response to JIRA_ENDPOINT/rest/api/2/serverInfo
  210. type jiraServiceInfo struct {
  211. ServerTitle string `json:"serverTitle"`
  212. Version string `json:"version"`
  213. VersionNumbers []int `json:"versionNumbers"`
  214. BaseURL string `json:"baseUrl"`
  215. }
  216. func jiraServerInfo(cli *jira.Client) (*jiraServiceInfo, error) {
  217. var jsi jiraServiceInfo
  218. req, _ := cli.NewRequest("GET", "rest/api/2/serverInfo", nil)
  219. _, err := cli.Do(req, &jsi)
  220. if err != nil {
  221. return nil, err
  222. }
  223. return &jsi, nil
  224. }
  225. func init() {
  226. types.RegisterAuthRealm(func(realmID string) types.AuthRealm {
  227. return &jiraRealm{id: realmID}
  228. })
  229. }