account api: infrastructure, login, register, getUser (#611)

This commit is contained in:
Yaroslav Vorobiov 2018-11-14 12:50:15 +02:00 committed by GitHub
parent 17519a7532
commit c442205b3a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 755 additions and 12 deletions

View File

@ -21,10 +21,11 @@ import (
"storj.io/storj/pkg/miniogw"
"storj.io/storj/pkg/overlay"
mock "storj.io/storj/pkg/overlay/mocks"
psserver "storj.io/storj/pkg/piecestore/psserver"
"storj.io/storj/pkg/piecestore/psserver"
"storj.io/storj/pkg/pointerdb"
"storj.io/storj/pkg/process"
"storj.io/storj/pkg/provider"
"storj.io/storj/pkg/satellite/satelliteweb"
"storj.io/storj/pkg/statdb"
"storj.io/storj/pkg/utils"
)
@ -43,6 +44,7 @@ type Satellite struct {
Repairer repairer.Config
Audit audit.Config
StatDB statdb.Config
Web satelliteweb.Config
MockOverlay struct {
Enabled bool `default:"true" help:"if false, use real overlay"`
Host string `default:"" help:"if set, the mock overlay will return storage nodes with this host"`
@ -131,6 +133,12 @@ func cmdRun(cmd *cobra.Command, args []string) (err error) {
if runCfg.Satellite.Audit.SatelliteAddr == "" {
runCfg.Satellite.Audit.SatelliteAddr = runCfg.Satellite.Identity.Address
}
if runCfg.Satellite.Web.SatelliteAddr == "" {
runCfg.Satellite.Web.SatelliteAddr = runCfg.Satellite.Identity.Address
}
// Run satellite
errch <- runCfg.Satellite.Identity.Run(ctx,
grpcauth.NewAPIKeyInterceptor(),
runCfg.Satellite.PointerDB,
@ -141,6 +149,7 @@ func cmdRun(cmd *cobra.Command, args []string) (err error) {
// TODO(coyle): re-enable the checker after we determine why it is panicing
// runCfg.Satellite.Checker,
runCfg.Satellite.Repairer,
runCfg.Satellite.Web,
)
}()

3
go.mod
View File

@ -102,7 +102,8 @@ require (
require (
github.com/garyburd/redigo v1.0.1-0.20170216214944-0d253a66e6e1 // indirect
github.com/go-ole/go-ole v1.2.1 // indirect
github.com/hanwen/go-fuse v0.0.0-20181011180456-b760b55765be
github.com/graphql-go/graphql v0.7.6
github.com/hanwen/go-fuse v0.0.0-20181027161220-c029b69a13a7
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/mattn/go-colorable v0.0.9 // indirect

6
go.sum
View File

@ -124,11 +124,13 @@ github.com/gorilla/mux v1.6.2 h1:Pgr17XVTNXAk3q/r4CpKzC5xBM/qW1uVLV+IhRZpIIk=
github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs=
github.com/gorilla/rpc v1.1.0 h1:marKfvVP0Gpd/jHlVBKCQ8RAoUPdX7K1Nuh6l1BNh7A=
github.com/gorilla/rpc v1.1.0/go.mod h1:V4h9r+4sF5HnzqbwIez0fKSpANP0zlYd3qR7p36jkTQ=
github.com/graphql-go/graphql v0.7.6 h1:3Bn1IFB5OvPoANEfu03azF8aMyks0G/H6G1XeTfYbM4=
github.com/graphql-go/graphql v0.7.6/go.mod h1:k6yrAYQaSP59DC5UVxbgxESlmVyojThKdORUqGDGmrI=
github.com/gtank/cryptopasta v0.0.0-20170601214702-1f550f6f2f69 h1:7xsUJsB2NrdcttQPa7JLEaGzvdbk7KvfrjgHZXOQRo0=
github.com/gtank/cryptopasta v0.0.0-20170601214702-1f550f6f2f69/go.mod h1:YLEMZOtU+AZ7dhN9T/IpGhXVGly2bvkJQ+zxj3WeVQo=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
github.com/hanwen/go-fuse v0.0.0-20181011180456-b760b55765be h1:RXF6Da5rbJlRUosxKxuy/3OrLLG77aXRrZYcjDs6aB4=
github.com/hanwen/go-fuse v0.0.0-20181011180456-b760b55765be/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok=
github.com/hanwen/go-fuse v0.0.0-20181027161220-c029b69a13a7 h1:+INF0+TK4ga3O+6Y0Z2ftiujA13KaCO/+kHN9V6Mj4A=
github.com/hanwen/go-fuse v0.0.0-20181027161220-c029b69a13a7/go.mod h1:unqXarDXqzAk0rt98O2tVndEPIpUgLD9+rwFisZH3Ok=
github.com/hashicorp/go-immutable-radix v1.0.0 h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0=
github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60=
github.com/hashicorp/go-msgpack v0.0.0-20150518234257-fa3f63826f7c h1:BTAbnbegUIMB6xmQCwWE8yRzbA4XSpnZY5hvRJC188I=

27
pkg/satellite/auth.go Normal file
View File

@ -0,0 +1,27 @@
package satellite
import (
"encoding/base64"
"storj.io/storj/pkg/satellite/satelliteauth"
)
//TODO: change to JWT or Macaroon based auth
// Signer creates signature for provided data
type Signer interface {
Sign(data []byte) ([]byte, error)
}
// signToken signs token with given signer
func signToken(token *satelliteauth.Token, signer Signer) error {
encoded := base64.URLEncoding.EncodeToString(token.Payload)
signature, err := signer.Sign([]byte(encoded))
if err != nil {
return err
}
token.Signature = signature
return nil
}

View File

@ -0,0 +1,38 @@
package satelliteauth
import (
"bytes"
"encoding/json"
"time"
"github.com/skyrings/skyring-common/tools/uuid"
)
//TODO: change to JWT or Macaroon based auth
// Claims represents data signed by server and used for authentication
type Claims struct {
ID uuid.UUID `json:"id"`
Email string `json:"email,omitempty"`
Expiration time.Time `json:"expires,omitempty"`
}
// JSON returns json representation of Claims
func (c *Claims) JSON() ([]byte, error) {
buffer := bytes.NewBuffer(nil)
err := json.NewEncoder(buffer).Encode(c)
return buffer.Bytes(), err
}
// FromJSON returns Claims instance, parsed from JSON
func FromJSON(data []byte) (*Claims, error) {
claims := new(Claims)
err := json.NewDecoder(bytes.NewReader(data)).Decode(claims)
if err != nil {
return nil, err
}
return claims, nil
}

View File

@ -0,0 +1,25 @@
package satelliteauth
import (
"crypto/hmac"
"crypto/sha256"
)
//TODO: change to JWT or Macaroon based auth
// Hmac is hmac256 based Signer
type Hmac struct {
Secret []byte
}
// Sign implements satellite signer
func (a *Hmac) Sign(data []byte) ([]byte, error) {
mac := hmac.New(sha256.New, a.Secret)
_, err := mac.Write(data)
if err != nil {
return nil, err
}
return mac.Sum(nil), nil
}

View File

@ -0,0 +1,52 @@
package satelliteauth
import (
"bytes"
"encoding/base64"
"io/ioutil"
"strings"
"github.com/zeebo/errs"
)
//TODO: change to JWT or Macaroon based auth
// Token represents authentication data structure
type Token struct {
Payload []byte
Signature []byte
}
// String returns base64URLEncoded data joined with .
func (t Token) String() string {
payload := base64.URLEncoding.EncodeToString(t.Payload)
signature := base64.URLEncoding.EncodeToString(t.Signature)
return strings.Join([]string{payload, signature}, ".")
}
// FromBase64URLString creates Token instance from base64URLEncoded string representation
func FromBase64URLString(token string) (Token, error) {
i := strings.Index(token, ".")
if i < 0 {
return Token{}, errs.New("invalid token format")
}
payload := token[:i]
signature := token[i+1:]
payloadDecoder := base64.NewDecoder(base64.URLEncoding, bytes.NewReader([]byte(payload)))
signatureDecoder := base64.NewDecoder(base64.URLEncoding, bytes.NewReader([]byte(signature)))
payloadBytes, err := ioutil.ReadAll(payloadDecoder)
if err != nil {
return Token{}, errs.New("decoding token's signature failed: %s", err)
}
signatureBytes, err := ioutil.ReadAll(signatureDecoder)
if err != nil {
return Token{}, errs.New("decoding token's body failed: %s", err)
}
return Token{Payload: payloadBytes, Signature: signatureBytes}, nil
}

View File

@ -6,11 +6,10 @@ package satellitedb
import (
"context"
"github.com/skyrings/skyring-common/tools/uuid"
"github.com/zeebo/errs"
"storj.io/storj/pkg/satellite"
"github.com/skyrings/skyring-common/tools/uuid"
"storj.io/storj/pkg/satellite/satellitedb/dbx"
)

View File

@ -0,0 +1,101 @@
package satelliteweb
import (
"context"
"encoding/json"
"io/ioutil"
"net/http"
"strings"
"github.com/graphql-go/graphql"
"github.com/zeebo/errs"
"storj.io/storj/pkg/auth"
"storj.io/storj/pkg/satellite/satelliteweb/satelliteql"
"storj.io/storj/pkg/utils"
)
const (
authorization = "Authorization"
contentType = "Content-Type"
authorizationBearer = "Bearer "
applicationJSON = "application/json"
applicationGraphql = "application/graphql"
)
func (gw *gateway) grapqlHandler(w http.ResponseWriter, req *http.Request) {
w.Header().Set(contentType, applicationJSON)
token := getToken(req)
query, err := getQuery(req)
if err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
result := graphql.Do(graphql.Params{
Schema: gw.schema,
Context: auth.WithAPIKey(context.Background(), []byte(token)),
RequestString: query,
})
if result.HasErrors() {
err = json.NewEncoder(w).Encode(result.Errors)
} else {
err = json.NewEncoder(w).Encode(result)
}
if err != nil {
gw.logger.Error(err)
return
}
gw.logger.Debug(result)
}
// getToken retrieves token from request
func getToken(req *http.Request) string {
value := req.Header.Get(authorization)
if value == "" {
return ""
}
if !strings.HasPrefix(value, authorizationBearer) {
return ""
}
return value[len(authorizationBearer):]
}
// getQuery retrieves graphql query from request
func getQuery(req *http.Request) (query string, err error) {
switch req.Method {
case http.MethodGet:
return req.URL.Query().Get(satelliteql.Query), nil
case http.MethodPost:
return queryPOST(req)
default:
return "", errs.New("wrong http request type")
}
}
// queryPOST retrieves query from POST request
func queryPOST(req *http.Request) (query string, err error) {
switch typ := req.Header.Get(contentType); typ {
case applicationGraphql:
body, err := ioutil.ReadAll(req.Body)
return string(body), utils.CombineErrors(err, req.Body.Close())
//TODO(yar): test more precisely
case applicationJSON:
var query struct {
Query string
}
err := json.NewDecoder(req.Body).Decode(&query)
return query.Query, utils.CombineErrors(err, req.Body.Close())
default:
return "", errs.New("can't parse request body of type %s", typ)
}
}

View File

@ -0,0 +1,75 @@
package satelliteweb
import (
"context"
"storj.io/storj/pkg/satellite"
"storj.io/storj/pkg/satellite/satelliteauth"
"go.uber.org/zap"
"github.com/graphql-go/graphql"
"storj.io/storj/pkg/provider"
"storj.io/storj/pkg/satellite/satellitedb"
"storj.io/storj/pkg/satellite/satelliteweb/satelliteql"
"storj.io/storj/pkg/utils"
)
// Config contains info needed for satellite account related services
type Config struct {
GatewayConfig
SatelliteAddr string `help:"satellite main endpoint" default:""`
DatabaseURL string `help:"" default:"sqlite3://$CONFDIR/satellitedb.db"`
}
// Run implements Responsibility interface
func (c Config) Run(ctx context.Context, server *provider.Provider) error {
sugar := zap.NewExample().Sugar()
// Create satellite DB
dbURL, err := utils.ParseURL(c.DatabaseURL)
if err != nil {
return err
}
db, err := satellitedb.New(dbURL.Scheme, dbURL.Path)
if err != nil {
return err
}
err = db.CreateTables()
sugar.Error(err)
service, err := satellite.NewService(
&satelliteauth.Hmac{Secret: []byte("my-suppa-secret-key")},
db,
)
if err != nil {
return err
}
creator := satelliteql.TypeCreator{}
err = creator.Create(service)
if err != nil {
return err
}
schema, err := graphql.NewSchema(graphql.SchemaConfig{
Query: creator.RootQuery(),
Mutation: creator.RootMutation(),
})
if err != nil {
return err
}
go (&gateway{
schema: schema,
config: c.GatewayConfig,
logger: sugar,
}).run()
return server.Run(ctx)
}

View File

@ -0,0 +1,41 @@
package satelliteweb
import (
"net/http"
"path/filepath"
"go.uber.org/zap"
"github.com/graphql-go/graphql"
)
// GatewayConfig contains configuration for gateway
type GatewayConfig struct {
Address string `help:"server address of the graphql api gateway and frontend app" default:"127.0.0.1:8081"`
StaticPath string `help:"path to static resources" default:""`
}
type gateway struct {
schema graphql.Schema
config GatewayConfig
logger *zap.SugaredLogger
}
func (gw *gateway) run() {
mux := http.NewServeMux()
fs := http.FileServer(http.Dir(gw.config.StaticPath))
mux.Handle("/api/graphql/v0", http.HandlerFunc(gw.grapqlHandler))
if gw.config.StaticPath != "" {
mux.Handle("/", http.HandlerFunc(gw.appHandler))
mux.Handle("/static/", http.StripPrefix("/static", fs))
}
err := http.ListenAndServe(gw.config.Address, mux)
gw.logger.Errorf("Unexpected exit of satellite gateway server: ", err)
}
func (gw *gateway) appHandler(w http.ResponseWriter, req *http.Request) {
http.ServeFile(w, req, filepath.Join(gw.config.StaticPath, "dist", "public", "index.html"))
}

View File

@ -0,0 +1,62 @@
package satelliteql
import (
"github.com/graphql-go/graphql"
"storj.io/storj/pkg/satellite"
)
const (
// Mutation is graphql request that modifies data
Mutation = "mutation"
registerMutation = "register"
)
// rootMutation creates mutation for graphql populated by AccountsClient
func rootMutation(service *satellite.Service, types Types) *graphql.Object {
return graphql.NewObject(graphql.ObjectConfig{
Name: Mutation,
Fields: graphql.Fields{
registerMutation: &graphql.Field{
Type: types.UserType(),
Args: graphql.FieldConfigArgument{
fieldEmail: &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
fieldPassword: &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
fieldFirstName: &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
fieldLastName: &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
email, _ := p.Args[fieldEmail].(string)
password, _ := p.Args[fieldPassword].(string)
firstName, _ := p.Args[fieldFirstName].(string)
lastName, _ := p.Args[fieldLastName].(string)
user, err := service.Register(
p.Context,
&satellite.User{
Email: email,
FirstName: firstName,
LastName: lastName,
PasswordHash: []byte(password),
},
)
if err != nil {
return nil, err
}
return user, nil
},
},
},
})
}

View File

@ -0,0 +1,70 @@
package satelliteql
import (
"github.com/graphql-go/graphql"
"github.com/skyrings/skyring-common/tools/uuid"
"storj.io/storj/pkg/satellite"
)
const (
// Query is immutable graphql request
Query = "query"
userQuery = "user"
tokenQuery = "token"
)
// rootQuery creates query for graphql populated by AccountsClient
func rootQuery(service *satellite.Service, types Types) *graphql.Object {
return graphql.NewObject(graphql.ObjectConfig{
Name: Query,
Fields: graphql.Fields{
userQuery: &graphql.Field{
Type: types.UserType(),
Args: graphql.FieldConfigArgument{
fieldID: &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
id, _ := p.Args[fieldID].(string)
idBytes, err := uuid.Parse(id)
if err != nil {
return nil, err
}
user, err := service.GetUser(p.Context, *idBytes)
if err != nil {
return nil, err
}
return user, nil
},
},
tokenQuery: &graphql.Field{
Type: graphql.String,
Args: graphql.FieldConfigArgument{
fieldEmail: &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
fieldPassword: &graphql.ArgumentConfig{
Type: graphql.NewNonNull(graphql.String),
},
},
Resolve: func(p graphql.ResolveParams) (interface{}, error) {
email, _ := p.Args[fieldEmail].(string)
pass, _ := p.Args[fieldPassword].(string)
token, err := service.Login(p.Context, email, pass)
if err != nil {
return nil, err
}
return token, nil
},
},
},
})
}

View File

@ -0,0 +1,57 @@
package satelliteql
import (
"github.com/graphql-go/graphql"
"storj.io/storj/pkg/satellite"
)
// Types return graphql type objects
type Types interface {
RootQuery() *graphql.Object
RootMutation() *graphql.Object
UserType() *graphql.Object
}
// TypeCreator handles graphql type creation and error checking
type TypeCreator struct {
query *graphql.Object
mutation *graphql.Object
user *graphql.Object
}
// RootQuery returns instance of query *graphql.Object
func (c *TypeCreator) RootQuery() *graphql.Object {
return c.query
}
// RootMutation returns instance of mutation *graphql.Object
func (c *TypeCreator) RootMutation() *graphql.Object {
return c.mutation
}
// Create create types and check for error
func (c *TypeCreator) Create(service *satellite.Service) error {
c.user = graphqlUser()
if err := c.user.Error(); err != nil {
return err
}
c.query = rootQuery(service, c)
if err := c.query.Error(); err != nil {
return err
}
c.mutation = rootMutation(service, c)
if err := c.mutation.Error(); err != nil {
return err
}
return nil
}
// UserType returns instance of user *graphql.Object
func (c *TypeCreator) UserType() *graphql.Object {
return c.user
}

View File

@ -0,0 +1,36 @@
package satelliteql
import (
"github.com/graphql-go/graphql"
)
const (
userType = "user"
fieldID = "id"
fieldEmail = "email"
fieldPassword = "password"
fieldFirstName = "firstName"
fieldLastName = "lastName"
)
// graphqlUser creates instance of user *graphql.Object
func graphqlUser() *graphql.Object {
return graphql.NewObject(graphql.ObjectConfig{
Name: userType,
Fields: graphql.Fields{
fieldID: &graphql.Field{
Type: graphql.String,
},
fieldEmail: &graphql.Field{
Type: graphql.String,
},
fieldFirstName: &graphql.Field{
Type: graphql.String,
},
fieldLastName: &graphql.Field{
Type: graphql.String,
},
},
})
}

148
pkg/satellite/service.go Normal file
View File

@ -0,0 +1,148 @@
package satellite
import (
"context"
"crypto/sha256"
"crypto/subtle"
"time"
"github.com/skyrings/skyring-common/tools/uuid"
"github.com/zeebo/errs"
"storj.io/storj/pkg/auth"
"storj.io/storj/pkg/satellite/satelliteauth"
)
// Service is handling accounts related logic
type Service struct {
Signer
store DB
}
// NewService returns new instance of Service
func NewService(signer Signer, store DB) (*Service, error) {
if signer == nil {
return nil, errs.New("signer can't be nil")
}
if store == nil {
return nil, errs.New("store can't be nil")
}
return &Service{Signer: signer, store: store}, nil
}
// Register gets password hash value and creates new user
func (s *Service) Register(ctx context.Context, user *User) (*User, error) {
passwordHash := sha256.Sum256(user.PasswordHash)
user.PasswordHash = passwordHash[:]
newUser, err := s.store.Users().Insert(ctx, user)
if err != nil {
return nil, err
}
return newUser, nil
}
// Login authenticates user by credentials and returns auth token
func (s *Service) Login(ctx context.Context, email, password string) (string, error) {
passwordHash := sha256.Sum256([]byte(password))
user, err := s.store.Users().GetByCredentials(ctx, passwordHash[:], email)
if err != nil {
return "", err
}
//TODO: move expiration time to constants
claims := satelliteauth.Claims{
ID: user.ID,
Expiration: time.Now().Add(time.Minute * 15),
}
token, err := s.createToken(&claims)
if err != nil {
return "", err
}
return token, nil
}
// GetUser returns user by id
func (s *Service) GetUser(ctx context.Context, id uuid.UUID) (*User, error) {
token, ok := auth.GetAPIKey(ctx)
if !ok {
return nil, errs.New("no api key was provided")
}
claims, err := s.authenticate(string(token))
if err != nil {
return nil, err
}
err = s.authorize(ctx, claims)
if err != nil {
return nil, err
}
user, err := s.store.Users().Get(ctx, id)
if err != nil {
return nil, err
}
return user, nil
}
func (s *Service) createToken(claims *satelliteauth.Claims) (string, error) {
json, err := claims.JSON()
if err != nil {
return "", err
}
token := satelliteauth.Token{Payload: json}
err = signToken(&token, s.Signer)
if err != nil {
return "", err
}
return token.String(), nil
}
func (s *Service) authenticate(tokenS string) (*satelliteauth.Claims, error) {
token, err := satelliteauth.FromBase64URLString(tokenS)
if err != nil {
return nil, err
}
signature := token.Signature
err = signToken(&token, s.Signer)
if err != nil {
return nil, err
}
if subtle.ConstantTimeCompare(signature, token.Signature) != 1 {
return nil, errs.New("incorrect signature")
}
claims, err := satelliteauth.FromJSON(token.Payload)
if err != nil {
return nil, err
}
return claims, nil
}
func (s *Service) authorize(ctx context.Context, claims *satelliteauth.Claims) error {
if !claims.Expiration.IsZero() && claims.Expiration.Before(time.Now()) {
return errs.New("token is outdated")
}
_, err := s.store.Users().Get(ctx, claims.ID)
if err != nil {
return errs.New("authorization failed. no user with id: %s", claims.ID.String())
}
return nil
}

View File

@ -26,12 +26,12 @@ type Users interface {
// User is a database object that describes User entity
type User struct {
ID uuid.UUID
ID uuid.UUID `json:"id"`
FirstName string
LastName string
Email string
PasswordHash []byte
FirstName string `json:"firstName"`
LastName string `json:"lastName"`
Email string `json:"email"`
PasswordHash []byte `json:"passwordHash"`
CreatedAt time.Time
CreatedAt time.Time `json:"createdAt"`
}