GraphQL API and utilities for the rpdata project
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

343 lines
9.2 KiB

3 years ago
3 years ago
  1. package services
  2. import (
  3. "context"
  4. "crypto/rand"
  5. "encoding/base64"
  6. "errors"
  7. "fmt"
  8. "git.aiterp.net/rpdata/api/models"
  9. "git.aiterp.net/rpdata/api/repositories"
  10. "github.com/dgrijalva/jwt-go"
  11. "log"
  12. "net/http"
  13. "reflect"
  14. "strings"
  15. "time"
  16. )
  17. var contextKey = &struct{ data string }{"Token Context Key"}
  18. // ErrNoKid is returned if the key id is missing from the jwt token header,
  19. var ErrNoKid = errors.New("missing \"kid\" field in token")
  20. // ErrKeyNotFound is returned if the key wasn't found.
  21. var ErrKeyNotFound = errors.New("key not found")
  22. // ErrInvalidClaims is returned by parseClaims if the claims cannot be parsed
  23. var ErrInvalidClaims = errors.New("invalid claims in token")
  24. // ErrExpired is returned by parseClaims if the expiry date is in the past
  25. var ErrExpired = errors.New("claims have already expired")
  26. // ErrWrongUser is returned by CheckToken if the key cannot represent this user
  27. var ErrWrongUser = errors.New("key is not valid for this user")
  28. // ErrDeletedUser is returned by CheckToken if the key can represent this user, but the user doesn't exist.
  29. var ErrDeletedUser = errors.New("user was not found")
  30. // ErrUnauthenticated is returned when the user is not authenticated
  31. var ErrUnauthenticated = errors.New("you are not authenticated")
  32. // ErrUnauthorized is returned when the user doesn't have access to this resource
  33. var ErrUnauthorized = errors.New("you are not authorized to perform this action")
  34. // ErrInvalidOperation is returned for operations that should never be allowed
  35. var ErrInvalidOperation = errors.New("no permission exists for this operation")
  36. // AuthService is a service for handling the 'orizations and 'entications.
  37. type AuthService struct {
  38. keys repositories.KeyRepository
  39. users repositories.UserRepository
  40. }
  41. // FindKey finds a key by id.
  42. func (s *AuthService) FindKey(ctx context.Context, id string) (*models.Key, error) {
  43. return s.keys.Find(ctx, id)
  44. }
  45. // FindUser finds a user by id.
  46. func (s *AuthService) FindUser(ctx context.Context, id string) (*models.User, error) {
  47. return s.users.Find(ctx, id)
  48. }
  49. // CreateKey generates a new key for the user and name. This
  50. // does not allow generating wildcard keys, they have to be
  51. // manually inserted into the DB.
  52. func (s *AuthService) CreateKey(ctx context.Context, name, user string) (*models.Key, error) {
  53. if user == "*" {
  54. return nil, errors.New("auth: wildcard keys not allowed")
  55. }
  56. secret, err := s.generateSecret()
  57. if err != nil {
  58. return nil, err
  59. }
  60. key := &models.Key{
  61. Name: name,
  62. User: user,
  63. Secret: secret,
  64. }
  65. key, err = s.keys.Insert(ctx, *key)
  66. if err != nil {
  67. return nil, err
  68. }
  69. return key, nil
  70. }
  71. // CheckPermission does some magic.
  72. func (s *AuthService) CheckPermission(ctx context.Context, op string, obj interface{}) error {
  73. token := s.TokenFromContext(ctx)
  74. if token == nil {
  75. return ErrUnauthenticated
  76. }
  77. if v := reflect.ValueOf(obj); v.Kind() == reflect.Struct {
  78. ptr := reflect.PtrTo(v.Type())
  79. ptrValue := reflect.New(ptr.Elem())
  80. ptrValue.Elem().Set(v)
  81. obj = ptrValue.Interface()
  82. }
  83. var authorized = false
  84. switch v := obj.(type) {
  85. case *models.Channel:
  86. authorized = token.Permitted("channel." + op)
  87. case *models.Character:
  88. authorized = token.PermittedUser(v.Author, "member", "character."+op)
  89. case *models.Chapter:
  90. authorized = token.PermittedUser(v.Author, "member", "chapter."+op)
  91. case *models.Comment:
  92. if op == "add" && v.Author != token.UserID {
  93. return ErrInvalidOperation
  94. }
  95. authorized = token.PermittedUser(v.Author, "member", "comment."+op)
  96. case *models.File:
  97. authorized = token.PermittedUser(v.Author, "member", "file."+op)
  98. case *models.Log:
  99. authorized = token.Permitted("log." + op)
  100. case *models.Post:
  101. authorized = token.Permitted("post." + op)
  102. case *models.Story:
  103. if op == "tag" && v.Open {
  104. authorized = true
  105. break
  106. }
  107. authorized = token.PermittedUser(v.Author, "member", "story."+op)
  108. case *models.User:
  109. authorized = token.Permitted("user." + op)
  110. default:
  111. log.Panicf("Invalid model %T: %#+v", v, v)
  112. }
  113. if !authorized {
  114. return ErrUnauthorized
  115. }
  116. return nil
  117. }
  118. // TokenFromContext gets the token from context.
  119. func (s *AuthService) TokenFromContext(ctx context.Context) *models.Token {
  120. token, ok := ctx.Value(contextKey).(*models.Token)
  121. if !ok {
  122. return nil
  123. }
  124. return token
  125. }
  126. // SpinOffContext adds the auth token to a background context.
  127. func (s *AuthService) SpinOffContext(ctx context.Context) context.Context {
  128. token := s.TokenFromContext(ctx)
  129. return context.WithValue(context.Background(), contextKey, &token)
  130. }
  131. // RequestWithToken either returns the request, or the request with a new context that
  132. // has the token.
  133. func (s *AuthService) RequestWithToken(r *http.Request) *http.Request {
  134. header := r.Header.Get("Authorization")
  135. if header == "" {
  136. return r
  137. }
  138. if !strings.HasPrefix(header, "Bearer ") {
  139. return r
  140. }
  141. token, err := s.CheckToken(r.Context(), header[7:])
  142. if err != nil {
  143. return r
  144. }
  145. return r.WithContext(context.WithValue(r.Context(), contextKey, &token))
  146. }
  147. // CheckToken reads the token string and returns a token if everything is kosher.
  148. func (s *AuthService) CheckToken(ctx context.Context, tokenString string) (token models.Token, err error) {
  149. var key *models.Key
  150. jwtToken, err := jwt.Parse(tokenString, func(jwtToken *jwt.Token) (interface{}, error) {
  151. if _, ok := jwtToken.Method.(*jwt.SigningMethodHMAC); !ok {
  152. return nil, fmt.Errorf("unexpected signing method: %v", jwtToken.Header["alg"])
  153. }
  154. kid, ok := jwtToken.Header["kid"].(string)
  155. if !ok {
  156. return nil, ErrNoKid
  157. }
  158. key, err = s.FindKey(ctx, kid)
  159. if err != nil || key.ID == "" {
  160. return nil, ErrKeyNotFound
  161. }
  162. return []byte(key.Secret), nil
  163. })
  164. if err != nil {
  165. return models.Token{}, err
  166. }
  167. userid, permissions, err := s.parseClaims(jwtToken.Claims)
  168. if err != nil {
  169. return models.Token{}, err
  170. }
  171. if !key.ValidForUser(userid) {
  172. return models.Token{}, ErrWrongUser
  173. }
  174. user, err := s.ensureUser(ctx, userid)
  175. if err != nil {
  176. return models.Token{}, ErrDeletedUser
  177. }
  178. acceptedPermissions := make([]string, 0, len(user.Permissions))
  179. if len(permissions) > 0 {
  180. for _, permission := range permissions {
  181. found := false
  182. for _, userPermission := range user.Permissions {
  183. if permission == userPermission {
  184. found = true
  185. break
  186. }
  187. }
  188. if found {
  189. acceptedPermissions = append(acceptedPermissions, permission)
  190. }
  191. }
  192. } else {
  193. acceptedPermissions = append(acceptedPermissions, user.Permissions...)
  194. }
  195. return models.Token{UserID: user.ID, Permissions: acceptedPermissions}, nil
  196. }
  197. // AllPermissions gets all permissions and their purpose
  198. func (s *AuthService) AllPermissions() map[string]string {
  199. return map[string]string{
  200. "member": "Can add/edit/remove own content",
  201. "user.edit": "Can edit non-owned users",
  202. "character.add": "Can add non-owned characters",
  203. "character.edit": "Can edit non-owned characters",
  204. "character.remove": "Can remove non-owned characters",
  205. "channel.add": "Can add channels",
  206. "channel.edit": "Can edit channels",
  207. "channel.remove": "Can remove channels",
  208. "comment.edit": "Can edit non-owned comments",
  209. "comment.remove": "Can remove non-owned comments",
  210. "log.add": "Can add logs",
  211. "log.edit": "Can edit logs",
  212. "log.remove": "Can remove logs",
  213. "post.add": "Can add posts",
  214. "post.edit": "Can edit posts",
  215. "post.move": "Can move posts",
  216. "post.remove": "Can remove posts",
  217. "story.add": "Can add non-owned stories",
  218. "story.edit": "Can edit non-owned stories",
  219. "story.remove": "Can remove non-owned stories",
  220. "story.unlisted": "Can view non-owned unlisted stories",
  221. "file.upload": "Can upload files",
  222. "file.list": "Can list non-owned files",
  223. "file.view": "Can view non-owned files",
  224. "file.edit": "Can edit non-owned files",
  225. "file.remove": "Can remove non-owned files",
  226. }
  227. }
  228. func (s *AuthService) parseClaims(jwtClaims jwt.Claims) (userid string, permissions []string, err error) {
  229. mapClaims, ok := jwtClaims.(jwt.MapClaims)
  230. if !ok {
  231. return "", nil, ErrInvalidClaims
  232. }
  233. if !mapClaims.VerifyExpiresAt(time.Now().Unix(), true) {
  234. return "", nil, ErrExpired
  235. }
  236. if userid, ok = mapClaims["user"].(string); !ok {
  237. return "", nil, ErrInvalidClaims
  238. }
  239. if claimedPermissions, ok := mapClaims["permissions"].([]interface{}); ok {
  240. for _, permission := range claimedPermissions {
  241. if permission, ok := permission.(string); ok {
  242. permissions = append(permissions, permission)
  243. }
  244. }
  245. if len(permissions) == 0 {
  246. return "", nil, ErrInvalidClaims
  247. }
  248. }
  249. return
  250. }
  251. func (s *AuthService) generateSecret() (string, error) {
  252. var data [64]byte
  253. _, err := rand.Read(data[:])
  254. if err != nil {
  255. return "", err
  256. }
  257. return base64.RawURLEncoding.EncodeToString(data[:]), nil
  258. }
  259. func (s *AuthService) ensureUser(ctx context.Context, id string) (*models.User, error) {
  260. user, err := s.users.Find(ctx, id)
  261. if err == repositories.ErrNotFound {
  262. user = &models.User{
  263. ID: id,
  264. Nick: "",
  265. Permissions: []string{
  266. "member",
  267. "log.edit",
  268. "post.edit",
  269. "post.move",
  270. "post.remove",
  271. "file.upload",
  272. },
  273. }
  274. user, err = s.users.Insert(ctx, *user)
  275. if err != nil {
  276. return nil, err
  277. }
  278. } else if err != nil {
  279. return nil, err
  280. }
  281. return user, err
  282. }