Browse Source

auth: Moved token and user to models package, added graphql endpoint to check token

1.0
Gisle Aune 6 years ago
parent
commit
2ab933149e
  1. 4
      graph2/gqlgen.yml
  2. 4
      graph2/graph.go
  3. 18
      graph2/queries/token.go
  4. 4
      graph2/schema/root.gql
  5. 8
      graph2/schema/types/Token.gql
  6. 8
      graph2/schema/types/User.gql
  7. 17
      graph2/types/token.go
  8. 69
      internal/auth/token.go
  9. 63
      internal/auth/user.go
  10. 48
      models/token.go
  11. 23
      models/user.go
  12. 14
      models/users/db.go
  13. 33
      models/users/ensure.go
  14. 13
      models/users/find.go

4
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

4
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
}

18
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
}

4
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.

8
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!]!
}

8
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!]!
}

17
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

69
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) {

63
internal/auth/user.go

@ -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")
})
}

48
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)
}

23
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
}

14
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")
})
}

33
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
}

13
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
}
Loading…
Cancel
Save