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.
329 lines
8.4 KiB
329 lines
8.4 KiB
package services
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"git.aiterp.net/stufflog/server/database/repositories"
|
|
"git.aiterp.net/stufflog/server/graph/loaders"
|
|
"git.aiterp.net/stufflog/server/internal/generate"
|
|
"git.aiterp.net/stufflog/server/internal/slerrors"
|
|
"git.aiterp.net/stufflog/server/models"
|
|
"github.com/gin-gonic/gin"
|
|
"math/rand"
|
|
"time"
|
|
)
|
|
|
|
var ErrLoginFailed = errors.New("login failed")
|
|
var ErrInternalLoginFailure = errors.New("login failed due to internal error")
|
|
var ErrInternalPermissionFailure = errors.New("permission check failed due to internal error")
|
|
var ErrWrongCurrentPassword = errors.New("current password is missing")
|
|
var ErrMissingCurrentPassword = errors.New("current password is missing")
|
|
|
|
var authCookieName = "stufflog_cookie"
|
|
var authCtxKey = "stufflog.auth"
|
|
var ginCtxKey = "stufflog.gin"
|
|
|
|
type Auth struct {
|
|
users repositories.UserRepository
|
|
session repositories.SessionRepository
|
|
projects repositories.ProjectRepository
|
|
issues repositories.IssueRepository
|
|
}
|
|
|
|
func (auth *Auth) Login(ctx context.Context, username, password string) (*models.User, error) {
|
|
user, err := auth.users.Find(ctx, username)
|
|
if err != nil {
|
|
select {
|
|
case <-time.After(time.Millisecond * time.Duration(rand.Int63n(100)+100)):
|
|
case <-ctx.Done():
|
|
}
|
|
return nil, ErrLoginFailed
|
|
}
|
|
|
|
if !user.CheckPassword(password) {
|
|
select {
|
|
case <-time.After(time.Millisecond * time.Duration(rand.Int63n(50))):
|
|
case <-ctx.Done():
|
|
}
|
|
return nil, ErrLoginFailed
|
|
}
|
|
|
|
session := models.Session{
|
|
ID: generate.SessionID(),
|
|
UserID: user.ID,
|
|
ExpiryTime: time.Now().Add(time.Hour * 168),
|
|
}
|
|
err = auth.session.Save(ctx, session)
|
|
if err != nil {
|
|
return nil, ErrInternalLoginFailure
|
|
}
|
|
|
|
if c := ctx.Value(ginCtxKey).(*gin.Context); c != nil {
|
|
c.SetCookie(authCookieName, session.ID, 3600*168, "/", "", false, true)
|
|
} else {
|
|
return nil, ErrInternalLoginFailure
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
func (auth *Auth) Logout(ctx context.Context) (*models.User, error) {
|
|
user := auth.UserFromContext(ctx)
|
|
if user == nil {
|
|
return nil, slerrors.PermissionDenied
|
|
}
|
|
|
|
c, ok := ctx.Value(ginCtxKey).(*gin.Context)
|
|
if !ok {
|
|
return nil, ErrInternalLoginFailure
|
|
}
|
|
|
|
c.SetCookie(authCookieName, "", 0, "/", "", false, true)
|
|
|
|
return user, nil
|
|
}
|
|
|
|
func (auth *Auth) CreateUser(ctx context.Context, username, password, name string, active, admin bool) (*models.User, error) {
|
|
loggedInUser := auth.UserFromContext(ctx)
|
|
if loggedInUser == nil || !loggedInUser.Admin {
|
|
return nil, slerrors.PermissionDenied
|
|
}
|
|
|
|
user := &models.User{
|
|
ID: username,
|
|
Name: name,
|
|
Active: active,
|
|
Admin: admin,
|
|
}
|
|
err := user.SetPassword(password)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
user, err = auth.users.Insert(ctx, *user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
func (auth *Auth) UserFromContext(ctx context.Context) *models.User {
|
|
user, _ := ctx.Value(authCtxKey).(*models.User)
|
|
return user
|
|
}
|
|
|
|
func (auth *Auth) ProjectPermission(ctx context.Context, projectID string) (*models.ProjectPermission, error) {
|
|
user := auth.UserFromContext(ctx)
|
|
if user == nil || !user.Active {
|
|
return nil, slerrors.PermissionDenied
|
|
}
|
|
|
|
permission, err := loaders.ProjectPermissionLoaderFromContext(ctx).Load(projectID)
|
|
if err != nil {
|
|
return nil, ErrInternalPermissionFailure
|
|
}
|
|
if permission.Level == models.ProjectPermissionLevelNoAccess {
|
|
return nil, slerrors.PermissionDenied
|
|
}
|
|
|
|
return permission, nil
|
|
}
|
|
|
|
func (auth *Auth) IssuePermission(ctx context.Context, issue models.Issue) (*models.ProjectPermission, error) {
|
|
user := auth.UserFromContext(ctx)
|
|
if user == nil || !user.Active {
|
|
return nil, slerrors.PermissionDenied
|
|
}
|
|
|
|
isOwnedOrAssigned := issue.AssigneeID == user.ID || issue.OwnerID == user.ID
|
|
|
|
permission, err := loaders.ProjectPermissionLoaderFromContext(ctx).Load(issue.ProjectID)
|
|
if err != nil {
|
|
return nil, ErrInternalPermissionFailure
|
|
}
|
|
if permission.Level == models.ProjectPermissionLevelNoAccess {
|
|
return nil, slerrors.PermissionDenied
|
|
}
|
|
if !(permission.CanViewAnyIssue() || (permission.CanViewOwnIssue() && isOwnedOrAssigned)) {
|
|
return nil, slerrors.PermissionDenied
|
|
}
|
|
|
|
return permission, nil
|
|
}
|
|
|
|
func (auth *Auth) CheckGinSession(c *gin.Context) {
|
|
ctx := context.WithValue(c.Request.Context(), ginCtxKey, c)
|
|
|
|
cookie, err := c.Cookie(authCookieName)
|
|
if err != nil {
|
|
c.Request = c.Request.WithContext(ctx)
|
|
return
|
|
}
|
|
|
|
session, err := auth.session.Find(c.Request.Context(), cookie)
|
|
if err != nil {
|
|
c.Request = c.Request.WithContext(ctx)
|
|
return
|
|
}
|
|
|
|
if time.Until(session.ExpiryTime) < time.Hour*167 {
|
|
session.ExpiryTime = time.Now().Add(time.Hour * 168)
|
|
_ = auth.session.Save(c.Request.Context(), *session)
|
|
c.SetCookie(authCookieName, session.ID, 3600*168, "/", "", false, true)
|
|
}
|
|
|
|
user, err := auth.users.Find(c.Request.Context(), session.UserID)
|
|
if err != nil {
|
|
c.Request = c.Request.WithContext(ctx)
|
|
return
|
|
}
|
|
|
|
ctx = context.WithValue(ctx, authCtxKey, user)
|
|
c.Request = c.Request.WithContext(ctx)
|
|
}
|
|
|
|
func (auth *Auth) EditUser(ctx context.Context, username string, setName *string, currentPassword *string, newPassword *string) (*models.User, error) {
|
|
loggedInUser := auth.UserFromContext(ctx)
|
|
if loggedInUser == nil {
|
|
return nil, slerrors.PermissionDenied
|
|
}
|
|
|
|
user, err := auth.users.Find(ctx, username)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if user.ID != loggedInUser.ID && !loggedInUser.Admin {
|
|
return nil, slerrors.PermissionDenied
|
|
}
|
|
|
|
if newPassword != nil {
|
|
// Only require current password if it's given, or if the user is NOT an admin changing
|
|
// another user's password
|
|
if currentPassword != nil || !(loggedInUser.Admin && loggedInUser.ID != user.ID) {
|
|
if currentPassword == nil {
|
|
return nil, ErrMissingCurrentPassword
|
|
}
|
|
if !user.CheckPassword(*currentPassword) {
|
|
return nil, ErrWrongCurrentPassword
|
|
}
|
|
}
|
|
|
|
err = user.SetPassword(*newPassword)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
if setName != nil && *setName != "" {
|
|
user.Name = *setName
|
|
}
|
|
|
|
err = auth.users.Save(ctx, *user)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return user, nil
|
|
}
|
|
|
|
func (auth *Auth) FilterLogListCopy(ctx context.Context, logs []*models.Log) []*models.Log {
|
|
logs2 := make([]*models.Log, 0, len(logs))
|
|
for _, log := range logs {
|
|
logs2 = append(logs2, log.Copy())
|
|
}
|
|
|
|
auth.FilterLogList(ctx, &logs2)
|
|
|
|
return logs2
|
|
}
|
|
|
|
func (auth *Auth) FilterLogList(ctx context.Context, logs *[]*models.Log) {
|
|
user := auth.UserFromContext(ctx)
|
|
if user == nil {
|
|
panic("Auth.FilterLogList called without user")
|
|
}
|
|
|
|
auth.FilterLog(ctx, *logs...)
|
|
deleteList := make([]int, 0, len(*logs)/2)
|
|
for i, log := range *logs {
|
|
if log.Empty() && log.UserID != user.ID {
|
|
deleteList = append(deleteList, i-len(deleteList))
|
|
}
|
|
}
|
|
|
|
list := *logs
|
|
for _, index := range deleteList {
|
|
list = append(list[:index], list[index+1:]...)
|
|
}
|
|
*logs = list
|
|
}
|
|
|
|
func (auth *Auth) FilterLog(ctx context.Context, logs ...*models.Log) {
|
|
userID := ""
|
|
if user := auth.UserFromContext(ctx); user != nil {
|
|
userID = user.ID
|
|
}
|
|
|
|
accessMap := make(map[string]bool)
|
|
deleteList := make([]int, 0, 16)
|
|
for _, log := range logs {
|
|
if userID == log.UserID {
|
|
continue
|
|
}
|
|
|
|
deleteList = deleteList[:0]
|
|
for i, item := range log.Items {
|
|
if access, ok := accessMap[item.IssueID]; ok && access {
|
|
continue
|
|
} else if ok && !access {
|
|
deleteList = append(deleteList, i-len(deleteList))
|
|
continue
|
|
}
|
|
|
|
issue, err := loaders.IssueLoaderFromContext(ctx).Load(item.IssueID)
|
|
if err != nil {
|
|
deleteList = append(deleteList, i-len(deleteList))
|
|
accessMap[item.IssueID] = true
|
|
continue
|
|
}
|
|
|
|
_, err = auth.IssuePermission(ctx, *issue)
|
|
if err != nil {
|
|
deleteList = append(deleteList, i-len(deleteList))
|
|
}
|
|
|
|
accessMap[issue.ID] = err != nil
|
|
}
|
|
for _, index := range deleteList {
|
|
log.Items = append(log.Items[:index], log.Items[index+1:]...)
|
|
}
|
|
deleteList = deleteList[:0]
|
|
for i, task := range log.Tasks {
|
|
if access, ok := accessMap[task.IssueID]; ok && access {
|
|
continue
|
|
} else if ok && !access {
|
|
deleteList = append(deleteList, i-len(deleteList))
|
|
continue
|
|
}
|
|
|
|
issue, err := loaders.IssueLoaderFromContext(ctx).Load(task.IssueID)
|
|
if err != nil {
|
|
deleteList = append(deleteList, i-len(deleteList))
|
|
accessMap[task.IssueID] = true
|
|
continue
|
|
}
|
|
|
|
_, err = auth.IssuePermission(ctx, *issue)
|
|
if err != nil {
|
|
deleteList = append(deleteList, i-len(deleteList))
|
|
}
|
|
|
|
accessMap[issue.ID] = err != nil
|
|
}
|
|
for _, index := range deleteList {
|
|
log.Tasks = append(log.Tasks[:index], log.Tasks[index+1:]...)
|
|
}
|
|
}
|
|
}
|