From 1c3518424a9d7e557a8e7cca11017bf164a365c7 Mon Sep 17 00:00:00 2001 From: Roland Osborne Date: Sun, 16 Jan 2022 21:11:24 -0800 Subject: [PATCH] added addAccount endpoint --- doc/api.oa3 | 16 ++-- net/server/go.mod | 3 + net/server/go.sum | 12 +++ net/server/internal/addAccount_endpoint.go | 77 +++++++++++++++++ net/server/internal/addAccount_test.go | 51 ++++++++---- net/server/internal/api_account.go | 5 -- net/server/internal/auth.go | 83 +++++++++++++++++++ net/server/internal/configNames.go | 1 + net/server/internal/logger.go | 5 ++ net/server/internal/main_test.go | 3 +- net/server/internal/models.go | 2 +- net/server/internal/setNodeClaim_endpoint.go | 15 +--- net/server/internal/setNodeConfig_endpoint.go | 23 ++++- net/server/internal/store/schema.go | 26 +++--- 14 files changed, 265 insertions(+), 57 deletions(-) create mode 100644 net/server/internal/addAccount_endpoint.go create mode 100644 net/server/internal/auth.go diff --git a/doc/api.oa3 b/doc/api.oa3 index c3c19016..d911c72f 100644 --- a/doc/api.oa3 +++ b/doc/api.oa3 @@ -75,7 +75,7 @@ paths: description: Set admin password and node domain operationId: set-node-claim security: - - basicAuth: [] + - basicCredentials: [] parameters: - name: domain in: query @@ -302,7 +302,7 @@ paths: operationId: add-public-account security: - bearerAuth: [] - - basicAuth: [] + - basicCredentials: [] responses: '201': description: successful operation @@ -431,7 +431,7 @@ paths: operationId: add-account security: - bearerAuth: [] - - basicAuth: [] + - basicCredentials: [] responses: '201': description: successful operation @@ -3964,11 +3964,11 @@ components: Profile: type: object required: - - profileId + - guid - revision - node properties: - profileId: + guid: type: string handle: type: string @@ -4468,7 +4468,11 @@ components: basicAuth: type: http scheme: basic - + + basicCredentials: + type: http + schema + bearerAuth: type: http scheme: bearer diff --git a/net/server/go.mod b/net/server/go.mod index 96168abd..6f6be907 100644 --- a/net/server/go.mod +++ b/net/server/go.mod @@ -7,7 +7,10 @@ require ( github.com/gorilla/websocket v1.4.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/now v1.1.3 // indirect + github.com/kr/pretty v0.3.0 // indirect + github.com/kr/text v0.2.0 // indirect github.com/mattn/go-sqlite3 v1.14.9 // indirect + github.com/rogpeppe/go-internal v1.6.1 // indirect github.com/theckman/go-securerandom v0.1.1 // indirect golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 // indirect gorm.io/driver/sqlite v1.2.6 // indirect diff --git a/net/server/go.sum b/net/server/go.sum index a5221543..1f395c5b 100644 --- a/net/server/go.sum +++ b/net/server/go.sum @@ -1,3 +1,4 @@ +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= @@ -9,9 +10,18 @@ github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkr github.com/jinzhu/now v1.1.2/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jinzhu/now v1.1.3 h1:PlHq1bSCSZL9K0wUhbm2pGLoTWs2GwVhsP6emvGV/ZI= github.com/jinzhu/now v1.1.3/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mattn/go-sqlite3 v1.14.9 h1:10HX2Td0ocZpYEjhilsuo6WWtUqttj2Kb0KtD86/KYA= github.com/mattn/go-sqlite3 v1.14.9/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= +github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/theckman/go-securerandom v0.1.1 h1:5KctSyM0D5KKFK+bsypIyLq7yik0CEaI5i2fGcUGcsQ= @@ -19,6 +29,8 @@ github.com/theckman/go-securerandom v0.1.1/go.mod h1:bmkysLfBH6i891sBpcP4xRM3XIB golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3 h1:0es+/5331RGQPcXlMfP+WrnIIS6dNnNRe0WB02W0F4M= golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gorm.io/driver/sqlite v1.2.6 h1:SStaH/b+280M7C8vXeZLz/zo9cLQmIGwwj3cSj7p6l4= gorm.io/driver/sqlite v1.2.6/go.mod h1:gyoX0vHiiwi0g49tv+x2E7l8ksauLK0U/gShcdUsjWY= diff --git a/net/server/internal/addAccount_endpoint.go b/net/server/internal/addAccount_endpoint.go new file mode 100644 index 00000000..2e54d09f --- /dev/null +++ b/net/server/internal/addAccount_endpoint.go @@ -0,0 +1,77 @@ +package databag + +import ( + "net/http" + "crypto/sha256" + "encoding/hex" + "databag/internal/store" +) + +func AddAccount(w http.ResponseWriter, r *http.Request) { + + if _, err := bearerAccountToken(r); err != nil { + LogMsg("authentication failed") + w.WriteHeader(http.StatusUnauthorized) + return + } + + username, password, err := basicCredentials(r); + if err != nil { + LogMsg("invalid basic credentials") + w.WriteHeader(http.StatusUnauthorized) + return + } + + // generate account key + privateKey, publicKey := GenerateRsaKeyPair() + privatePem := ExportRsaPrivateKeyAsPemStr(privateKey) + publicPem, err := ExportRsaPublicKeyAsPemStr(publicKey) + if err != nil { + LogMsg("failed generate key") + w.WriteHeader(http.StatusInternalServerError) + return + } + + // compute key fingerprint + msg := []byte(publicPem) + hash := sha256.New() + if _, err = hash.Write(msg); err != nil { + LogMsg("failed to fingerprint key") + w.WriteHeader(http.StatusInternalServerError) + return + } + fingerprint := hex.EncodeToString(hash.Sum(nil)) + + // create new account + account := store.Account{ + PublicKey: publicPem, + PrivateKey: privatePem, + KeyType: "RSA4096", + Username: username, + Password: password, + Guid: fingerprint, + }; + if res := store.DB.Create(&account).Error; res != nil { + LogMsg("failed to store account") + w.WriteHeader(http.StatusInternalServerError) + return + } + + // create response + profile := Profile{ + Guid: account.Guid, + Handle: account.Username, + Name: account.Name, + Description: account.Description, + Location: account.Location, + Image: account.Image, + Revision: account.ProfileRevision, + Version: CONFIG_VERSION, + Node: "https://" + getStrConfigValue(CONFIG_DOMAIN, ""), + } + + // send response + WriteResponse(w, profile) +} + + diff --git a/net/server/internal/addAccount_test.go b/net/server/internal/addAccount_test.go index 7c1cd5f4..a155b3f6 100644 --- a/net/server/internal/addAccount_test.go +++ b/net/server/internal/addAccount_test.go @@ -14,11 +14,11 @@ func TestAccount(t *testing.T) { r := httptest.NewRequest("POST", "/admin/accounts", nil) r.Header.Add("Authorization","Basic " + auth) w := httptest.NewRecorder() - AddNodeAccount(w, r); - resp := w.Result(); - dec := json.NewDecoder(resp.Body); - var token string; - dec.Decode(&token); + AddNodeAccount(w, r) + resp := w.Result() + dec := json.NewDecoder(resp.Body) + var token string + dec.Decode(&token) if resp.StatusCode != 200 { t.Errorf("failed to create account") return @@ -28,36 +28,55 @@ func TestAccount(t *testing.T) { r = httptest.NewRequest("GET", "/account/token", nil) r.Header.Add("Authorization","Bearer " + token) w = httptest.NewRecorder() - GetAccountToken(w, r); - resp = w.Result(); + GetAccountToken(w, r) + resp = w.Result() if resp.StatusCode != 200 { t.Errorf("invalid token value") return } - dec = json.NewDecoder(resp.Body); - var tokenType string; - dec.Decode(&tokenType); + dec = json.NewDecoder(resp.Body) + var tokenType string + dec.Decode(&tokenType) if tokenType != "create" { t.Errorf("invalid token type") return } // check if username is available - r = httptest.NewRequest("GET", "/account/claimable?username=databag", nil) + r = httptest.NewRequest("GET", "/account/claimable?username=user", nil) r.Header.Add("Authorization","Bearer " + token) w = httptest.NewRecorder() - GetAccountUsername(w, r); - resp = w.Result(); + GetAccountUsername(w, r) + resp = w.Result() if resp.StatusCode != 200 { t.Errorf("invalid token value") return } - dec = json.NewDecoder(resp.Body); - var available bool; - dec.Decode(&available); + dec = json.NewDecoder(resp.Body) + var available bool + dec.Decode(&available) if !available { t.Errorf("username not available") return } + // create account + auth = base64.StdEncoding.EncodeToString([]byte("user:pass")) + r = httptest.NewRequest("GET", "/account/profile", nil) + r.Header.Add("Credentials","Basic " + auth) + r.Header.Add("Authorization","Bearer " + token) + w = httptest.NewRecorder() + AddAccount(w, r) + resp = w.Result() + if resp.StatusCode != 200 { + t.Errorf("invalid token value") + return + } + dec = json.NewDecoder(resp.Body) + var profile Profile + dec.Decode(&profile) + if profile.Guid == nil { + t.Errorf("invalid profile") + return + } } diff --git a/net/server/internal/api_account.go b/net/server/internal/api_account.go index 5c32b905..9234e355 100644 --- a/net/server/internal/api_account.go +++ b/net/server/internal/api_account.go @@ -13,11 +13,6 @@ import ( "net/http" ) -func AddAccount(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json; charset=UTF-8") - w.WriteHeader(http.StatusOK) -} - func AddAccountApp(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json; charset=UTF-8") w.WriteHeader(http.StatusOK) diff --git a/net/server/internal/auth.go b/net/server/internal/auth.go new file mode 100644 index 00000000..38a2d994 --- /dev/null +++ b/net/server/internal/auth.go @@ -0,0 +1,83 @@ +package databag + +import ( + "errors" + "strings" + "net/http" + "encoding/base64" + "golang.org/x/crypto/bcrypt" + "databag/internal/store" +) + +func adminLogin(r *http.Request) bool { + + // extract request auth + username, password, ok := r.BasicAuth(); + if !ok || username == "" || password == "" { + return false + } + + // nothing to do if not configured + if !getBoolConfigValue(CONFIG_CONFIGURED, false) { + return false; + } + + // compare username + if getStrConfigValue(CONFIG_USERNAME, "") != username { + return false + } + + // compare password + p := getBinConfigValue(CONFIG_PASSWORD, nil); + if bcrypt.CompareHashAndPassword(p, []byte(password)) != nil { + return false + } + + return true; +} + +func bearerAccountToken(r *http.Request) (store.AccountToken, error) { + + // parse bearer authentication + auth := r.Header.Get("Authorization") + token := strings.TrimSpace(strings.TrimPrefix(auth, "Bearer")) + + // find token record + var accountToken store.AccountToken + err := store.DB.Where("token = ?", token).First(&accountToken).Error + return accountToken, err +} + +func basicCredentials(r *http.Request) (string, []byte, error) { + + var username string + var password []byte + + // parse bearer authentication + auth := r.Header.Get("Credentials") + token := strings.TrimSpace(strings.TrimPrefix(auth, "Basic")) + + // decode basic auth + credentials, err := base64.StdEncoding.DecodeString(token) + if err != nil { + LogMsg("faield to decode basic credentials"); + return username, password, err + } + + // parse credentials + login := strings.Split(string(credentials), ":"); + if login[0] == "" || login[1] == "" { + LogMsg("failed to parse basic credentials"); + return username, password, errors.New("invalid credentials") + } + username = login[0] + + // hash password + password, err = bcrypt.GenerateFromPassword([]byte(login[1]), bcrypt.DefaultCost) + if err != nil { + LogMsg("failed to hash password") + return username, password, err + } + + return username, password, nil +} diff --git a/net/server/internal/configNames.go b/net/server/internal/configNames.go index 371c386d..350926b3 100644 --- a/net/server/internal/configNames.go +++ b/net/server/internal/configNames.go @@ -1,6 +1,7 @@ package databag const CONFIG_BODYLIMIT = 1048576 +const CONFIG_VERSION = "0.0.1" const CONFIG_CONFIGURED = "configured" const CONFIG_USERNAME = "username" diff --git a/net/server/internal/logger.go b/net/server/internal/logger.go index 48d1b3bd..3faf1192 100644 --- a/net/server/internal/logger.go +++ b/net/server/internal/logger.go @@ -16,6 +16,7 @@ import ( "os" "runtime" "strings" + "github.com/kr/pretty" ) func Logger(inner http.Handler, name string) http.Handler { @@ -39,3 +40,7 @@ func LogMsg(msg string) { p, _ := os.Getwd() log.Printf("%s:%d %s", strings.TrimPrefix(file, p), line, msg) } + +func PrintMsg(obj interface{}) { + pretty.Println(obj); +} diff --git a/net/server/internal/main_test.go b/net/server/internal/main_test.go index 259e1238..e27b9190 100644 --- a/net/server/internal/main_test.go +++ b/net/server/internal/main_test.go @@ -43,11 +43,10 @@ func Claimable() { func Claim() { auth := base64.StdEncoding.EncodeToString([]byte("admin:pass")) r := httptest.NewRequest("PUT", "/admin/claim", nil) - r.Header.Add("Authorization","Basic " + auth) + r.Header.Add("Credentials","Basic " + auth) w := httptest.NewRecorder() SetNodeClaim(w, r) if w.Code != 200 { -LogMsg("HERE"); panic("server not initially claimable") } } diff --git a/net/server/internal/models.go b/net/server/internal/models.go index 7e2bb0fc..268e0c1c 100644 --- a/net/server/internal/models.go +++ b/net/server/internal/models.go @@ -230,7 +230,7 @@ type NodeConfig struct { } type Profile struct { - ProfileId string `json:"profileId"` + Guid string `json:"profileId"` Handle string `json:"handle,omitempty"` Name string `json:"name,omitempty"` Description string `json:"description,omitempty"` diff --git a/net/server/internal/setNodeClaim_endpoint.go b/net/server/internal/setNodeClaim_endpoint.go index 52fb3338..267180a4 100644 --- a/net/server/internal/setNodeClaim_endpoint.go +++ b/net/server/internal/setNodeClaim_endpoint.go @@ -5,7 +5,6 @@ import ( "net/http" "gorm.io/gorm" "databag/internal/store" - "golang.org/x/crypto/bcrypt" ) func SetNodeClaim(w http.ResponseWriter, r *http.Request) { @@ -21,24 +20,18 @@ func SetNodeClaim(w http.ResponseWriter, r *http.Request) { return } - username, password, ok := r.BasicAuth(); - if !ok || username == "" || password == "" { - LogMsg("SetNodeClaim - invalid credenitals"); + username, password, res := basicCredentials(r); + if res != nil { + LogMsg("invalid credenitals"); w.WriteHeader(http.StatusBadRequest) return } - hashedPassword, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) - if err != nil { - LogMsg("SetNodeClaim - failed to hash password"); - w.WriteHeader(http.StatusInternalServerError) - return - } err = store.DB.Transaction(func(tx *gorm.DB) error { if res := tx.Create(&store.Config{ConfigId: CONFIG_USERNAME, StrValue: username}).Error; res != nil { return res } - if res := tx.Create(&store.Config{ConfigId: CONFIG_PASSWORD, BinValue: hashedPassword}).Error; res != nil { + if res := tx.Create(&store.Config{ConfigId: CONFIG_PASSWORD, BinValue: password}).Error; res != nil { return res } if res := tx.Create(&store.Config{ConfigId: CONFIG_CONFIGURED, BoolValue: true}).Error; res != nil { diff --git a/net/server/internal/setNodeConfig_endpoint.go b/net/server/internal/setNodeConfig_endpoint.go index 2c5b03dd..c315b818 100644 --- a/net/server/internal/setNodeConfig_endpoint.go +++ b/net/server/internal/setNodeConfig_endpoint.go @@ -5,6 +5,7 @@ import ( "encoding/json" "net/http" "gorm.io/gorm" + "gorm.io/gorm/clause" "databag/internal/store" ) @@ -30,15 +31,31 @@ func SetNodeConfig(w http.ResponseWriter, r *http.Request) { // store credentials err := store.DB.Transaction(func(tx *gorm.DB) error { - if res := tx.Create(&store.Config{ConfigId: CONFIG_DOMAIN, StrValue: config.Domain}).Error; res != nil { + + // upsert domain config + if res := tx.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "config_id"}}, + DoUpdates: clause.AssignmentColumns([]string{"str_value"}), + }).Create(&store.Config{ConfigId: CONFIG_DOMAIN, StrValue: config.Domain}).Error; res != nil { return res } - if res := tx.Create(&store.Config{ConfigId: CONFIG_PUBLICLIMIT, NumValue: config.PublicLimit}).Error; res != nil { + + // upsert public limit config + if res := tx.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "config_id"}}, + DoUpdates: clause.AssignmentColumns([]string{"num_value"}), + }).Create(&store.Config{ConfigId: CONFIG_PUBLICLIMIT, NumValue: config.PublicLimit}).Error; res != nil { return res } - if res := tx.Create(&store.Config{ConfigId: CONFIG_STORAGE, NumValue: config.AccountStorage}).Error; res != nil { + + // upsert account storage config + if res := tx.Clauses(clause.OnConflict{ + Columns: []clause.Column{{Name: "config_id"}}, + DoUpdates: clause.AssignmentColumns([]string{"num_value"}), + }).Create(&store.Config{ConfigId: CONFIG_STORAGE, NumValue: config.AccountStorage}).Error; res != nil { return res } + return nil; }) if(err != nil) { diff --git a/net/server/internal/store/schema.go b/net/server/internal/store/schema.go index 5271f791..361f2593 100644 --- a/net/server/internal/store/schema.go +++ b/net/server/internal/store/schema.go @@ -46,24 +46,24 @@ type AccountToken struct { type Account struct { ID uint `gorm:"primaryKey;not null;unique;autoIncrement"` - PublicKey []byte `gorm:"not null"` - PrivateKey []byte `gorm:"not null"` - KeyType []byte `gorm:"not null"` - ProfileId string `gorm:"not null;uniqueIndex"` + PublicKey string `gorm:"not null"` + PrivateKey string `gorm:"not null"` + KeyType string `gorm:"not null"` + Guid string `gorm:"not null;uniqueIndex"` Username string `gorm:"not null;uniqueIndex"` Password []byte `gorm:"not null"` Name string Description string Location string Image string - ProfileRevision int64 `gorm:"not null"` - ContentRevision int64 `gorm:"not null"` - ViewRevision int64 `gorm:"not null"` - GroupRevision int64 `gorm:"not null"` - LabelRevision int64 `gorm:"not null"` - CardRevision int64 `gorm:"not null"` - DialogueRevision int64 `gorm:"not null"` - InsightRevision uint64 `gorm:"not null"` + ProfileRevision int64 `gorm:"not null;default:1"` + ContentRevision int64 `gorm:"not null;default:1"` + ViewRevision int64 `gorm:"not null;default:1"` + GroupRevision int64 `gorm:"not null;default:1"` + LabelRevision int64 `gorm:"not null;default:1"` + CardRevision int64 `gorm:"not null;default:1"` + DialogueRevision int64 `gorm:"not null;default:1"` + InsightRevision uint64 `gorm:"not null;default:1"` Created int64 `gorm:"autoCreateTime"` Apps []App } @@ -108,7 +108,7 @@ type Card struct { ID uint `gorm:"primaryKey;not null;unique;autoIncrement"` CardId string `gorm:"not null;index:card,unique"` AccountID uint `gorm:"not null;index:card,unique"` - DID string `gorm:"not null"` + GUID string `gorm:"not null"` Username string Name string Description string