diff --git a/graph2/gqlgen.yml b/graph2/gqlgen.yml index 1780a60..c94e1af 100644 --- a/graph2/gqlgen.yml +++ b/graph2/gqlgen.yml @@ -49,5 +49,9 @@ models: model: git.aiterp.net/rpdata/api/models.File FilesFilter: model: git.aiterp.net/rpdata/api/models/files.Filter + Token: + model: git.aiterp.net/rpdata/api/models.Token + User: + model: git.aiterp.net/rpdata/api/models.User Date: model: git.aiterp.net/rpdata/api/models/scalars.Date \ No newline at end of file diff --git a/graph2/graph.go b/graph2/graph.go index d0fde54..684a7c0 100644 --- a/graph2/graph.go +++ b/graph2/graph.go @@ -37,3 +37,7 @@ func (r *rootResolver) Story() StoryResolver { func (r *rootResolver) File() FileResolver { return &types.FileResolver } + +func (r *rootResolver) Token() TokenResolver { + return &types.TokenResolver +} diff --git a/graph2/queries/token.go b/graph2/queries/token.go new file mode 100644 index 0000000..893a0fd --- /dev/null +++ b/graph2/queries/token.go @@ -0,0 +1,18 @@ +package queries + +import ( + "context" + "errors" + + "git.aiterp.net/rpdata/api/internal/auth" + "git.aiterp.net/rpdata/api/models" +) + +func (r *resolver) Token(ctx context.Context) (models.Token, error) { + token := auth.TokenFromContext(ctx) + if !token.Authenticated() { + return models.Token{}, errors.New("No (valid) token") + } + + return *token, nil +} diff --git a/graph2/schema/root.gql b/graph2/schema/root.gql index 1fbe10e..c9497a7 100644 --- a/graph2/schema/root.gql +++ b/graph2/schema/root.gql @@ -51,6 +51,10 @@ type Query { # Find files files(filter: FilesFilter): [File!]! + + + # Get information about the token, useful for debugging. + token: Token! } # A Date represents a RFC3339 encoded date with up to millisecond precision. diff --git a/graph2/schema/types/Token.gql b/graph2/schema/types/Token.gql new file mode 100644 index 0000000..bc3c948 --- /dev/null +++ b/graph2/schema/types/Token.gql @@ -0,0 +1,8 @@ +# Token represents the data parsed and validated from the bearer token. +type Token { + # The user represented in the token. + user: User! + + # The permissions for the token. + permissions: [String!]! +} \ No newline at end of file diff --git a/graph2/schema/types/User.gql b/graph2/schema/types/User.gql new file mode 100644 index 0000000..f396ca6 --- /dev/null +++ b/graph2/schema/types/User.gql @@ -0,0 +1,8 @@ +# A user represents a member on the RPData platform. +type User { + # The user's unique ID / username + id: String! + + # What the user is permitted to do. + permissions: [String!]! +} \ No newline at end of file diff --git a/graph2/types/token.go b/graph2/types/token.go new file mode 100644 index 0000000..bfa4264 --- /dev/null +++ b/graph2/types/token.go @@ -0,0 +1,17 @@ +package types + +import ( + "context" + + "git.aiterp.net/rpdata/api/models" + "git.aiterp.net/rpdata/api/models/users" +) + +type tokenResolver struct{} + +func (r *tokenResolver) User(ctx context.Context, token *models.Token) (models.User, error) { + return users.Find(token.UserID) +} + +// TokenResolver is a resolver +var TokenResolver tokenResolver diff --git a/internal/auth/token.go b/internal/auth/token.go index 5133a56..212d2c3 100644 --- a/internal/auth/token.go +++ b/internal/auth/token.go @@ -8,6 +8,8 @@ import ( "strings" "time" + "git.aiterp.net/rpdata/api/models" + "git.aiterp.net/rpdata/api/models/users" jwt "github.com/dgrijalva/jwt-go" ) @@ -34,56 +36,9 @@ var ErrWrongPermissions = errors.New("User does not have these permissions") // ErrDeletedUser is returned by CheckToken if the key can represent this user, but the user doesn't exist. var ErrDeletedUser = errors.New("User was not found") -// A Token contains the parsed results from an bearer token. Its methods are safe to use with a nil receiver, but -// the userID should be checked. -type Token struct { - UserID string - Permissions []string -} - -// Authenticated returns true if the token is non-nil and parsed -func (token *Token) Authenticated() bool { - return token != nil && token.UserID != "" -} - -// Permitted returns true if the token is non-nil and has the given permission or the "admin" permission -func (token *Token) Permitted(permissions ...string) bool { - if token == nil { - return false - } - - for _, tokenPermission := range token.Permissions { - if tokenPermission == "admin" { - return true - } - - for _, permission := range permissions { - if permission == tokenPermission { - return true - } - } - } - - return false -} - -// PermittedUser checks the first permission if the user matches, the second otherwise. This is a common -// pattern. -func (token *Token) PermittedUser(userID, permissionIfUser, permissionOtherwise string) bool { - if token == nil { - return false - } - - if token.UserID == userID { - return token.Permitted(permissionIfUser) - } - - return token.Permitted(permissionOtherwise) -} - // TokenFromContext gets the token from context. -func TokenFromContext(ctx context.Context) *Token { - token, ok := ctx.Value(contextKey).(*Token) +func TokenFromContext(ctx context.Context) *models.Token { + token, ok := ctx.Value(contextKey).(*models.Token) if !ok { return nil } @@ -112,7 +67,7 @@ func RequestWithToken(r *http.Request) *http.Request { } // CheckToken reads the token string and returns a token if everything is kosher. -func CheckToken(tokenString string) (token Token, err error) { +func CheckToken(tokenString string) (token models.Token, err error) { var key Key jwtToken, err := jwt.Parse(tokenString, func(jwtToken *jwt.Token) (interface{}, error) { @@ -133,21 +88,21 @@ func CheckToken(tokenString string) (token Token, err error) { return []byte(key.Secret), nil }) if err != nil { - return Token{}, err + return models.Token{}, err } userid, permissions, err := parseClaims(jwtToken.Claims) if err != nil { - return Token{}, err + return models.Token{}, err } if !key.ValidForUser(userid) { - return Token{}, ErrWrongUser + return models.Token{}, ErrWrongUser } - user, err := FindUser(userid) + user, err := users.Ensure(userid) if err != nil { - return Token{}, ErrDeletedUser + return models.Token{}, ErrDeletedUser } for _, permission := range permissions { @@ -161,11 +116,11 @@ func CheckToken(tokenString string) (token Token, err error) { } if !found { - return Token{}, ErrWrongPermissions + return models.Token{}, ErrWrongPermissions } } - return Token{UserID: token.UserID, Permissions: permissions}, nil + return models.Token{UserID: user.ID, Permissions: permissions}, nil } func parseClaims(jwtClaims jwt.Claims) (userid string, permissions []string, err error) { diff --git a/internal/auth/user.go b/internal/auth/user.go deleted file mode 100644 index 94a4eb5..0000000 --- a/internal/auth/user.go +++ /dev/null @@ -1,63 +0,0 @@ -package auth - -import ( - "git.aiterp.net/rpdata/api/internal/store" - "github.com/globalsign/mgo" -) - -var userCollection *mgo.Collection - -// A User represents user information about a user that has logged in. -type User struct { - ID string `bson:"_id" json:"id"` - Nick string `bson:"nick,omitempty" json:"nick,omitempty"` - Permissions []string `bson:"permissions" json:"permissions"` -} - -// Permitted returns true if either of the permissions can be found -// -// `token.UserID == page.Author || token.Permitted("story.edit")` -func (user *User) Permitted(permissions ...string) bool { - for i := range permissions { - for j := range user.Permissions { - if permissions[i] == user.Permissions[j] { - return true - } - } - } - - return false -} - -// FindUser finds a user by userid -func FindUser(userid string) (User, error) { - user := User{} - err := userCollection.FindId(userid).One(&user) - - if err == mgo.ErrNotFound { - user := User{ - ID: userid, - Nick: "", - Permissions: []string{ - "member", - "log.edit", - "post.edit", - "post.move", - "file.upload", - }, - } - - err := userCollection.Insert(user) - if err != nil { - return User{}, err - } - } - - return user, err -} - -func init() { - store.HandleInit(func(db *mgo.Database) { - userCollection = db.C("core.users") - }) -} diff --git a/models/token.go b/models/token.go new file mode 100644 index 0000000..c4af355 --- /dev/null +++ b/models/token.go @@ -0,0 +1,48 @@ +package models + +// A Token contains the parsed results from an bearer token. Its methods are safe to use with a nil receiver, but +// the userID should be checked. +type Token struct { + UserID string + Permissions []string +} + +// Authenticated returns true if the token is non-nil and parsed +func (token *Token) Authenticated() bool { + return token != nil && token.UserID != "" +} + +// Permitted returns true if the token is non-nil and has the given permission or the "admin" permission +func (token *Token) Permitted(permissions ...string) bool { + if token == nil { + return false + } + + for _, tokenPermission := range token.Permissions { + if tokenPermission == "admin" { + return true + } + + for _, permission := range permissions { + if permission == tokenPermission { + return true + } + } + } + + return false +} + +// PermittedUser checks the first permission if the user matches, the second otherwise. This is a common +// pattern. +func (token *Token) PermittedUser(userID, permissionIfUser, permissionOtherwise string) bool { + if token == nil { + return false + } + + if token.UserID == userID { + return token.Permitted(permissionIfUser) + } + + return token.Permitted(permissionOtherwise) +} diff --git a/models/user.go b/models/user.go new file mode 100644 index 0000000..eecf91b --- /dev/null +++ b/models/user.go @@ -0,0 +1,23 @@ +package models + +// A User represents user information about a user that has logged in. +type User struct { + ID string `bson:"_id" json:"id"` + Nick string `bson:"nick,omitempty" json:"nick,omitempty"` + Permissions []string `bson:"permissions" json:"permissions"` +} + +// Permitted returns true if either of the permissions can be found +// +// `token.UserID == page.Author || token.Permitted("story.edit")` +func (user *User) Permitted(permissions ...string) bool { + for i := range permissions { + for j := range user.Permissions { + if permissions[i] == user.Permissions[j] { + return true + } + } + } + + return false +} diff --git a/models/users/db.go b/models/users/db.go new file mode 100644 index 0000000..dca3310 --- /dev/null +++ b/models/users/db.go @@ -0,0 +1,14 @@ +package users + +import ( + "git.aiterp.net/rpdata/api/internal/store" + "github.com/globalsign/mgo" +) + +var collection *mgo.Collection + +func init() { + store.HandleInit(func(db *mgo.Database) { + collection = db.C("core.users") + }) +} diff --git a/models/users/ensure.go b/models/users/ensure.go new file mode 100644 index 0000000..5b0425f --- /dev/null +++ b/models/users/ensure.go @@ -0,0 +1,33 @@ +package users + +import ( + "git.aiterp.net/rpdata/api/models" + "github.com/globalsign/mgo" +) + +// Ensure finds a user by id, or makes a new one. +func Ensure(id string) (models.User, error) { + user := models.User{} + err := collection.FindId(id).One(&user) + + if err == mgo.ErrNotFound { + user = models.User{ + ID: id, + Nick: "", + Permissions: []string{ + "member", + "log.edit", + "post.edit", + "post.move", + "file.upload", + }, + } + + err := collection.Insert(user) + if err != nil { + return models.User{}, err + } + } + + return user, err +} diff --git a/models/users/find.go b/models/users/find.go new file mode 100644 index 0000000..a6fd5c4 --- /dev/null +++ b/models/users/find.go @@ -0,0 +1,13 @@ +package users + +import ( + "git.aiterp.net/rpdata/api/models" +) + +// Find finds a user by id +func Find(id string) (models.User, error) { + user := models.User{} + err := collection.FindId(id).One(&user) + + return user, err +}