|
|
package services
import ( "context" "crypto/rand" "encoding/base64" "errors" "fmt" "git.aiterp.net/rpdata/api/models" "git.aiterp.net/rpdata/api/repositories" "github.com/dgrijalva/jwt-go" "log" "net/http" "reflect" "strings" "time" )
var contextKey = &struct{ data string }{"Token Context Key"}
// ErrNoKid is returned if the key id is missing from the jwt token header,
var ErrNoKid = errors.New("missing \"kid\" field in token")
// ErrKeyNotFound is returned if the key wasn't found.
var ErrKeyNotFound = errors.New("key not found")
// ErrInvalidClaims is returned by parseClaims if the claims cannot be parsed
var ErrInvalidClaims = errors.New("invalid claims in token")
// ErrExpired is returned by parseClaims if the expiry date is in the past
var ErrExpired = errors.New("claims have already expired")
// ErrWrongUser is returned by CheckToken if the key cannot represent this user
var ErrWrongUser = errors.New("key is not valid for this user")
// 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")
// ErrUnauthenticated is returned when the user is not authenticated
var ErrUnauthenticated = errors.New("you are not authenticated")
// ErrUnauthorized is returned when the user doesn't have access to this resource
var ErrUnauthorized = errors.New("you are not authorized to perform this action")
// ErrInvalidOperation is returned for operations that should never be allowed
var ErrInvalidOperation = errors.New("no permission exists for this operation")
// AuthService is a service for handling the 'orizations and 'entications.
type AuthService struct { keys repositories.KeyRepository users repositories.UserRepository }
// FindKey finds a key by id.
func (s *AuthService) FindKey(ctx context.Context, id string) (*models.Key, error) { return s.keys.Find(ctx, id) }
// FindUser finds a user by id.
func (s *AuthService) FindUser(ctx context.Context, id string) (*models.User, error) { return s.users.Find(ctx, id) }
// CreateKey generates a new key for the user and name. This
// does not allow generating wildcard keys, they have to be
// manually inserted into the DB.
func (s *AuthService) CreateKey(ctx context.Context, name, user string) (*models.Key, error) { if user == "*" { return nil, errors.New("auth: wildcard keys not allowed") }
secret, err := s.generateSecret() if err != nil { return nil, err }
key := &models.Key{ Name: name, User: user, Secret: secret, }
key, err = s.keys.Insert(ctx, *key) if err != nil { return nil, err }
return key, nil }
// CheckPermission does some magic.
func (s *AuthService) CheckPermission(ctx context.Context, op string, obj interface{}) error { token := s.TokenFromContext(ctx) if token == nil { return ErrUnauthenticated }
if v := reflect.ValueOf(obj); v.Kind() == reflect.Struct { ptr := reflect.PtrTo(v.Type()) ptrValue := reflect.New(ptr.Elem()) ptrValue.Elem().Set(v)
obj = ptrValue.Interface() }
var authorized = false
switch v := obj.(type) { case *models.Channel: authorized = token.Permitted("channel." + op) case *models.Character: authorized = token.PermittedUser(v.Author, "member", "character."+op) case *models.Chapter: authorized = token.PermittedUser(v.Author, "member", "chapter."+op) case *models.Comment: if op == "add" && v.Author != token.UserID { return ErrInvalidOperation }
authorized = token.PermittedUser(v.Author, "member", "comment."+op) case *models.File: authorized = token.PermittedUser(v.Author, "member", "file."+op) case *models.Log: authorized = token.Permitted("log." + op) case *models.Post: authorized = token.Permitted("post." + op) case *models.Story: if op == "tag" && v.Open { authorized = true break }
authorized = token.PermittedUser(v.Author, "member", "story."+op) case *models.User: authorized = token.Permitted("user." + op) default: log.Panicf("Invalid model %T: %#+v", v, v) }
if !authorized { return ErrUnauthorized }
return nil }
// TokenFromContext gets the token from context.
func (s *AuthService) TokenFromContext(ctx context.Context) *models.Token { token, ok := ctx.Value(contextKey).(*models.Token) if !ok { return nil }
return token }
// SpinOffContext adds the auth token to a background context.
func (s *AuthService) SpinOffContext(ctx context.Context) context.Context { token := s.TokenFromContext(ctx)
return context.WithValue(context.Background(), contextKey, &token) }
// RequestWithToken either returns the request, or the request with a new context that
// has the token.
func (s *AuthService) RequestWithToken(r *http.Request) *http.Request { header := r.Header.Get("Authorization") if header == "" { return r }
if !strings.HasPrefix(header, "Bearer ") { return r }
token, err := s.CheckToken(r.Context(), header[7:]) if err != nil { return r }
return r.WithContext(context.WithValue(r.Context(), contextKey, &token)) }
// CheckToken reads the token string and returns a token if everything is kosher.
func (s *AuthService) CheckToken(ctx context.Context, tokenString string) (token models.Token, err error) { var key *models.Key
jwtToken, err := jwt.Parse(tokenString, func(jwtToken *jwt.Token) (interface{}, error) { if _, ok := jwtToken.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("unexpected signing method: %v", jwtToken.Header["alg"]) }
kid, ok := jwtToken.Header["kid"].(string) if !ok { return nil, ErrNoKid }
key, err = s.FindKey(ctx, kid) if err != nil || key.ID == "" { return nil, ErrKeyNotFound }
return []byte(key.Secret), nil }) if err != nil { return models.Token{}, err }
userid, permissions, err := s.parseClaims(jwtToken.Claims) if err != nil { return models.Token{}, err }
if !key.ValidForUser(userid) { return models.Token{}, ErrWrongUser }
user, err := s.ensureUser(ctx, userid) if err != nil { return models.Token{}, ErrDeletedUser }
acceptedPermissions := make([]string, 0, len(user.Permissions)) if len(permissions) > 0 { for _, permission := range permissions { found := false
for _, userPermission := range user.Permissions { if permission == userPermission { found = true break } }
if found { acceptedPermissions = append(acceptedPermissions, permission) } } } else { acceptedPermissions = append(acceptedPermissions, user.Permissions...) }
return models.Token{UserID: user.ID, Permissions: acceptedPermissions}, nil }
// AllPermissions gets all permissions and their purpose
func (s *AuthService) AllPermissions() map[string]string { return map[string]string{ "member": "Can add/edit/remove own content", "user.edit": "Can edit non-owned users", "character.add": "Can add non-owned characters", "character.edit": "Can edit non-owned characters", "character.remove": "Can remove non-owned characters", "channel.add": "Can add channels", "channel.edit": "Can edit channels", "channel.remove": "Can remove channels", "comment.edit": "Can edit non-owned comments", "comment.remove": "Can remove non-owned comments", "log.add": "Can add logs", "log.edit": "Can edit logs", "log.remove": "Can remove logs", "post.add": "Can add posts", "post.edit": "Can edit posts", "post.move": "Can move posts", "post.remove": "Can remove posts", "story.add": "Can add non-owned stories", "story.edit": "Can edit non-owned stories", "story.remove": "Can remove non-owned stories", "story.unlisted": "Can view non-owned unlisted stories", "file.upload": "Can upload files", "file.list": "Can list non-owned files", "file.view": "Can view non-owned files", "file.edit": "Can edit non-owned files", "file.remove": "Can remove non-owned files", } }
func (s *AuthService) parseClaims(jwtClaims jwt.Claims) (userid string, permissions []string, err error) { mapClaims, ok := jwtClaims.(jwt.MapClaims) if !ok { return "", nil, ErrInvalidClaims }
if !mapClaims.VerifyExpiresAt(time.Now().Unix(), true) { return "", nil, ErrExpired }
if userid, ok = mapClaims["user"].(string); !ok { return "", nil, ErrInvalidClaims }
if claimedPermissions, ok := mapClaims["permissions"].([]interface{}); ok { for _, permission := range claimedPermissions { if permission, ok := permission.(string); ok { permissions = append(permissions, permission) } }
if len(permissions) == 0 { return "", nil, ErrInvalidClaims } }
return }
func (s *AuthService) generateSecret() (string, error) { var data [64]byte
_, err := rand.Read(data[:]) if err != nil { return "", err }
return base64.RawURLEncoding.EncodeToString(data[:]), nil }
func (s *AuthService) ensureUser(ctx context.Context, id string) (*models.User, error) { user, err := s.users.Find(ctx, id) if err == repositories.ErrNotFound { user = &models.User{ ID: id, Nick: "", Permissions: []string{ "member", "log.edit", "post.edit", "post.move", "post.remove", "file.upload", }, }
user, err = s.users.Insert(ctx, *user) if err != nil { return nil, err } } else if err != nil { return nil, err }
return user, err }
|