diff --git a/src/github.com/matrix-org/go-neb/api.go b/src/github.com/matrix-org/go-neb/api.go index 92383a8..3699608 100644 --- a/src/github.com/matrix-org/go-neb/api.go +++ b/src/github.com/matrix-org/go-neb/api.go @@ -17,6 +17,32 @@ func (*heartbeatHandler) OnIncomingRequest(req *http.Request) (interface{}, *err return &struct{}{}, nil } +type configureAuthHandler struct { + db *database.ServiceDB +} + +func (*configureAuthHandler) OnIncomingRequest(req *http.Request) (interface{}, *errors.HTTPError) { + if req.Method != "POST" { + return nil, &errors.HTTPError{nil, "Unsupported Method", 405} + } + var tpa types.ThirdPartyAuth + if err := json.NewDecoder(req.Body).Decode(&tpa); err != nil { + return nil, &errors.HTTPError{err, "Error parsing request JSON", 400} + } + + am := types.GetAuthModule(tpa.Type) + if am == nil { + return nil, &errors.HTTPError{nil, "Bad auth type: " + tpa.Type, 400} + } + + err := am.Process(tpa) + if err != nil { + return nil, &errors.HTTPError{err, "Failed to persist auth", 500} + } + + return nil, nil +} + type webhookHandler struct { db *database.ServiceDB } diff --git a/src/github.com/matrix-org/go-neb/auth/auth.go b/src/github.com/matrix-org/go-neb/auth/auth.go new file mode 100644 index 0000000..eb9338d --- /dev/null +++ b/src/github.com/matrix-org/go-neb/auth/auth.go @@ -0,0 +1,13 @@ +package auth + +import ( + "github.com/matrix-org/go-neb/auth/github" + "github.com/matrix-org/go-neb/database" + "github.com/matrix-org/go-neb/types" +) + +// RegisterModules registers all known modules so they can be retrieved via +// type.GetAuthModule +func RegisterModules(db *database.ServiceDB) { + types.RegisterAuthModule(&github.AuthModule{Database: db}) +} diff --git a/src/github.com/matrix-org/go-neb/auth/github/github.go b/src/github.com/matrix-org/go-neb/auth/github/github.go new file mode 100644 index 0000000..3661417 --- /dev/null +++ b/src/github.com/matrix-org/go-neb/auth/github/github.go @@ -0,0 +1,22 @@ +package github + +import ( + "github.com/matrix-org/go-neb/database" + "github.com/matrix-org/go-neb/types" +) + +// AuthModule for github +type AuthModule struct { + Database *database.ServiceDB +} + +// Type of the auth module +func (*AuthModule) Type() string { + return "github" +} + +// Process a third-party auth request +func (am *AuthModule) Process(tpa types.ThirdPartyAuth) (err error) { + _, err = am.Database.StoreThirdPartyAuth(tpa) + return +} diff --git a/src/github.com/matrix-org/go-neb/database/db.go b/src/github.com/matrix-org/go-neb/database/db.go index ece6d23..2259534 100644 --- a/src/github.com/matrix-org/go-neb/database/db.go +++ b/src/github.com/matrix-org/go-neb/database/db.go @@ -95,6 +95,38 @@ func (d *ServiceDB) LoadServicesInRoom(serviceUserID, roomID string) (services [ return } +// LoadThirdPartyAuth loads third-party credentials that the given userID +// has linked to the given resource. Returns sql.ErrNoRows if there are no +// credentials for the given resource/user combination. +func (d *ServiceDB) LoadThirdPartyAuth(resource, userID string) (tpa types.ThirdPartyAuth, err error) { + err = runTransaction(d.db, func(txn *sql.Tx) error { + tpa, err = selectThirdPartyAuthTxn(txn, resource, userID) + if err != nil { + return err + } + return nil + }) + return +} + +// StoreThirdPartyAuth stores the ThirdPartyAuth for the given Service. Updates the +// time added/updated values. +// If the auth already exists then it will be updated, otherwise a new auth +// will be inserted. The previous auth is returned. +func (d *ServiceDB) StoreThirdPartyAuth(tpa types.ThirdPartyAuth) (old types.ThirdPartyAuth, err error) { + err = runTransaction(d.db, func(txn *sql.Tx) error { + old, err = selectThirdPartyAuthTxn(txn, tpa.Resource, tpa.UserID) + if err == sql.ErrNoRows { + return insertThirdPartyAuthTxn(txn, tpa) + } else if err != nil { + return err + } else { + return updateThirdPartyAuthTxn(txn, tpa) + } + }) + return +} + // StoreService stores a service into the database either by inserting a new // service or updating an existing service. Returns the old service if there // was one. diff --git a/src/github.com/matrix-org/go-neb/database/schema.go b/src/github.com/matrix-org/go-neb/database/schema.go index 420766f..70275d0 100644 --- a/src/github.com/matrix-org/go-neb/database/schema.go +++ b/src/github.com/matrix-org/go-neb/database/schema.go @@ -34,6 +34,16 @@ CREATE TABLE IF NOT EXISTS matrix_clients ( time_updated_ms BIGINT NOT NULL, UNIQUE(user_id) ); + +CREATE TABLE IF NOT EXISTS third_party_auth ( + user_id TEXT NOT NULL, + type TEXT NOT NULL, + resource TEXT NOT NULL, + auth_json TEXT NOT NULL, + time_added_ms BIGINT NOT NULL, + time_updated_ms BIGINT NOT NULL, + UNIQUE(user_id, resource) +); ` const selectServiceUserIDsSQL = ` @@ -201,3 +211,39 @@ func selectRoomServicesTxn(txn *sql.Tx, serviceUserID, roomID string) (serviceID } return } + +const selectThirdPartyAuthSQL = ` +SELECT type, auth_json FROM third_party_auth WHERE user_id=$1 AND resource=$2 +` + +func selectThirdPartyAuthTxn(txn *sql.Tx, resource, userID string) (tpa types.ThirdPartyAuth, err error) { + tpa.Resource = resource + tpa.UserID = userID + err = txn.QueryRow(selectThirdPartyAuthSQL, userID, resource).Scan(&tpa.Type, &tpa.AuthJSON) + return +} + +const insertThirdPartyAuthSQL = ` +INSERT INTO third_party_auth( + user_id, type, resource, auth_json, time_added_ms, time_updated_ms +) VALUES($1, $2, $3, $4, $5, $6) +` + +func insertThirdPartyAuthTxn(txn *sql.Tx, tpa types.ThirdPartyAuth) (err error) { + timeAddedMs := time.Now().UnixNano() / 1000000 + _, err = txn.Exec(insertThirdPartyAuthSQL, tpa.UserID, tpa.Type, tpa.Resource, + []byte(tpa.AuthJSON), timeAddedMs, timeAddedMs) + return +} + +const updateThirdPartyAuthSQL = ` +UPDATE third_party_auth SET auth_json=$1, time_updated_ms=$2 + WHERE user_id=$3 AND resource=$4 +` + +func updateThirdPartyAuthTxn(txn *sql.Tx, tpa types.ThirdPartyAuth) (err error) { + timeUpdatedMs := time.Now().UnixNano() / 1000000 + _, err = txn.Exec(updateThirdPartyAuthSQL, []byte(tpa.AuthJSON), timeUpdatedMs, + tpa.UserID, tpa.Resource) + return err +} diff --git a/src/github.com/matrix-org/go-neb/goneb.go b/src/github.com/matrix-org/go-neb/goneb.go index 6515e6d..ca45dc8 100644 --- a/src/github.com/matrix-org/go-neb/goneb.go +++ b/src/github.com/matrix-org/go-neb/goneb.go @@ -2,6 +2,7 @@ package main import ( log "github.com/Sirupsen/logrus" + "github.com/matrix-org/go-neb/auth" "github.com/matrix-org/go-neb/clients" "github.com/matrix-org/go-neb/database" "github.com/matrix-org/go-neb/server" @@ -28,10 +29,12 @@ func main() { log.Panic(err) } + auth.RegisterModules(db) + http.Handle("/test", server.MakeJSONAPI(&heartbeatHandler{})) http.Handle("/admin/configureClient", server.MakeJSONAPI(&configureClientHandler{db: db, clients: clients})) http.Handle("/admin/configureService", server.MakeJSONAPI(&configureServiceHandler{db: db, clients: clients})) - + http.Handle("/admin/configureAuth", server.MakeJSONAPI(&configureAuthHandler{db: db})) wh := &webhookHandler{db: db} http.HandleFunc("/services/hooks/", wh.handle) diff --git a/src/github.com/matrix-org/go-neb/types/types.go b/src/github.com/matrix-org/go-neb/types/types.go index dcf42e6..1f0f6c9 100644 --- a/src/github.com/matrix-org/go-neb/types/types.go +++ b/src/github.com/matrix-org/go-neb/types/types.go @@ -1,6 +1,7 @@ package types import ( + "encoding/json" "errors" "github.com/matrix-org/go-neb/plugin" "net/http" @@ -51,3 +52,38 @@ func CreateService(serviceID, serviceType string) Service { } return f(serviceID) } + +// AuthModule represents a thing which can handle auth requests of a given type. +type AuthModule interface { + Type() string + Process(tpa ThirdPartyAuth) error +} + +var authModulesByType = map[string]AuthModule{} + +// ThirdPartyAuth represents an individual authorisation entry between +// a third party and the Matrix user. +type ThirdPartyAuth struct { + // The ID of the matrix user who has authed with the third party + UserID string + // The type of auth (e.g. "jira", "github"). This determines which + // auth module is loaded to process the auth. + Type string + // The location of the third party resource e.g. "github.com". + // This is mainly relevant for decentralised services like JIRA which + // may have many different locations (e.g. "matrix.org/jira") for the + // same ServiceType ("jira"). + Resource string + // An opaque JSON blob of stored auth data. + AuthJSON json.RawMessage +} + +// RegisterAuthModule so it can be used by other parts of NEB. +func RegisterAuthModule(am AuthModule) { + authModulesByType[am.Type()] = am +} + +// GetAuthModule for the given auth type. Returns nil if no match. +func GetAuthModule(authType string) AuthModule { + return authModulesByType[authType] +}