diff --git a/src/github.com/matrix-org/go-neb/api.go b/src/github.com/matrix-org/go-neb/api.go index 8f4f1c9..8a4097a 100644 --- a/src/github.com/matrix-org/go-neb/api.go +++ b/src/github.com/matrix-org/go-neb/api.go @@ -17,30 +17,47 @@ func (*heartbeatHandler) OnIncomingRequest(req *http.Request) (interface{}, *err return &struct{}{}, nil } -type configureAuthHandler struct { +type configureAuthRealmHandler struct { db *database.ServiceDB } -func (*configureAuthHandler) OnIncomingRequest(req *http.Request) (interface{}, *errors.HTTPError) { +func (h *configureAuthRealmHandler) 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 { + var body struct { + ID string + Type string + Config json.RawMessage + } + if err := json.NewDecoder(req.Body).Decode(&body); 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} + if body.ID == "" || body.Type == "" || body.Config == nil { + return nil, &errors.HTTPError{nil, `Must supply a "ID", a "Type" and a "Config"`, 400} + } + + realm := types.CreateAuthRealm(body.ID, body.Type) + if realm == nil { + return nil, &errors.HTTPError{nil, "Unknown realm type", 400} + } + + if err := json.Unmarshal(body.Config, realm); err != nil { + return nil, &errors.HTTPError{err, "Error parsing config JSON", 400} } - err := am.Process(tpa) + oldRealm, err := h.db.StoreAuthRealm(realm) if err != nil { - return nil, &errors.HTTPError{err, "Failed to persist auth", 500} + return nil, &errors.HTTPError{err, "Error storing realm", 500} } - return nil, nil + return &struct { + ID string + Type string + OldConfig types.AuthRealm + NewConfig types.AuthRealm + }{body.ID, body.Type, oldRealm, realm}, nil } type webhookHandler struct { 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 2259534..76b2add 100644 --- a/src/github.com/matrix-org/go-neb/database/db.go +++ b/src/github.com/matrix-org/go-neb/database/db.go @@ -191,6 +191,33 @@ func (d *ServiceDB) StoreService(service types.Service, client *matrix.Client) ( return } +// LoadAuthRealm loads an AuthRealm from the database. +// Returns sql.ErrNoRows if the realm isn't in the database. +func (d *ServiceDB) LoadAuthRealm(realmID string) (realm types.AuthRealm, err error) { + err = runTransaction(d.db, func(txn *sql.Tx) error { + realm, err = selectRealmTxn(txn, realmID) + return err + }) + return +} + +// StoreAuthRealm stores the given AuthRealm, clobbering based on the realm ID. +// This function updates the time added/updated values. The previous realm, if any, is +// returned. +func (d *ServiceDB) StoreAuthRealm(realm types.AuthRealm) (old types.AuthRealm, err error) { + err = runTransaction(d.db, func(txn *sql.Tx) error { + old, err = selectRealmTxn(txn, realm.ID()) + if err == sql.ErrNoRows { + return insertRealmTxn(txn, time.Now(), realm) + } else if err != nil { + return err + } else { + return updateRealmTxn(txn, time.Now(), realm) + } + }) + return +} + func runTransaction(db *sql.DB, fn func(txn *sql.Tx) error) (err error) { txn, err := db.Begin() if err != nil { 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 70275d0..36b424c 100644 --- a/src/github.com/matrix-org/go-neb/database/schema.go +++ b/src/github.com/matrix-org/go-neb/database/schema.go @@ -44,6 +44,15 @@ CREATE TABLE IF NOT EXISTS third_party_auth ( time_updated_ms BIGINT NOT NULL, UNIQUE(user_id, resource) ); + +CREATE TABLE IF NOT EXISTS auth_realms ( + realm_id TEXT NOT NULL, + realm_type TEXT NOT NULL, + realm_json TEXT NOT NULL, + time_added_ms BIGINT NOT NULL, + time_updated_ms BIGINT NOT NULL, + UNIQUE(realm_id) +); ` const selectServiceUserIDsSQL = ` @@ -247,3 +256,61 @@ func updateThirdPartyAuthTxn(txn *sql.Tx, tpa types.ThirdPartyAuth) (err error) tpa.UserID, tpa.Resource) return err } + +const insertRealmSQL = ` +INSERT INTO auth_realms( + realm_id, realm_type, realm_json, time_added_ms, time_updated_ms +) VALUES ($1, $2, $3, $4, $5) +` + +func insertRealmTxn(txn *sql.Tx, now time.Time, realm types.AuthRealm) error { + realmJSON, err := json.Marshal(realm) + if err != nil { + return err + } + t := now.UnixNano() / 1000000 + _, err = txn.Exec( + insertRealmSQL, + realm.ID(), realm.Type(), realmJSON, t, t, + ) + return err +} + +const selectRealmSQL = ` +SELECT realm_type, realm_json FROM auth_realms WHERE realm_id = $1 +` + +func selectRealmTxn(txn *sql.Tx, realmID string) (types.AuthRealm, error) { + var realmType string + var realmJSON []byte + row := txn.QueryRow(selectRealmSQL, realmID) + if err := row.Scan(&realmType, &realmJSON); err != nil { + return nil, err + } + realm := types.CreateAuthRealm(realmID, realmType) + if realm == nil { + return nil, fmt.Errorf("Cannot create realm of type %s", realmType) + } + if err := json.Unmarshal(realmJSON, realm); err != nil { + return nil, err + } + return realm, nil +} + +const updateRealmSQL = ` +UPDATE auth_realms SET realm_type=$1, realm_json=$2, time_updated_ms=$3 + WHERE realm_id=$4 +` + +func updateRealmTxn(txn *sql.Tx, now time.Time, realm types.AuthRealm) error { + realmJSON, err := json.Marshal(realm) + if err != nil { + return err + } + t := now.UnixNano() / 1000000 + _, err = txn.Exec( + updateRealmSQL, realm.Type(), realmJSON, t, + realm.ID(), + ) + 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 8e983a7..f23428a 100644 --- a/src/github.com/matrix-org/go-neb/goneb.go +++ b/src/github.com/matrix-org/go-neb/goneb.go @@ -5,6 +5,7 @@ import ( "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/realms/github" "github.com/matrix-org/go-neb/server" _ "github.com/matrix-org/go-neb/services/echo" _ "github.com/matrix-org/go-neb/services/github" @@ -34,7 +35,7 @@ func main() { 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})) + http.Handle("/admin/configureAuthRealm", server.MakeJSONAPI(&configureAuthRealmHandler{db: db})) wh := &webhookHandler{db: db, clients: clients} http.HandleFunc("/services/hooks/", wh.handle) diff --git a/src/github.com/matrix-org/go-neb/realms/github/github.go b/src/github.com/matrix-org/go-neb/realms/github/github.go new file mode 100644 index 0000000..cb59f09 --- /dev/null +++ b/src/github.com/matrix-org/go-neb/realms/github/github.go @@ -0,0 +1,26 @@ +package realms + +import ( + "github.com/matrix-org/go-neb/types" +) + +type githubRealm struct { + id string + ClientSecret string + ClientID string + WebhookEndpoint string +} + +func (r *githubRealm) ID() string { + return r.id +} + +func (r *githubRealm) Type() string { + return "github" +} + +func init() { + types.RegisterAuthRealm(func(realmID string) types.AuthRealm { + return &githubRealm{id: realmID} + }) +} 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 c63a15a..e5c9fff 100644 --- a/src/github.com/matrix-org/go-neb/types/types.go +++ b/src/github.com/matrix-org/go-neb/types/types.go @@ -88,3 +88,27 @@ func RegisterAuthModule(am AuthModule) { func GetAuthModule(authType string) AuthModule { return authModulesByType[authType] } + +// AuthRealm represents a place where a user can authenticate themselves. +// This may static (like github.com) or a specific domain (like matrix.org/jira) +type AuthRealm interface { + ID() string + Type() string +} + +var realmsByType = map[string]func(string) AuthRealm{} + +// RegisterAuthRealm registers a factory for creating AuthRealm instances. +func RegisterAuthRealm(factory func(string) AuthRealm) { + realmsByType[factory("").Type()] = factory +} + +// CreateAuthRealm creates an AuthRealm of the given type and realm ID. +// Returns nil if the realm couldn't be created. +func CreateAuthRealm(realmID, realmType string) AuthRealm { + f := realmsByType[realmType] + if f == nil { + return nil + } + return f(realmID) +}