github.com/nais/knorten@v0.0.0-20240104110906-55926958e361/pkg/api/team.go (about)

     1  package api
     2  
     3  import (
     4  	"crypto/rand"
     5  	"database/sql"
     6  	"encoding/hex"
     7  	"errors"
     8  	"fmt"
     9  	"net/http"
    10  	"net/mail"
    11  	"regexp"
    12  	"strconv"
    13  	"strings"
    14  
    15  	"github.com/gin-contrib/sessions"
    16  	"github.com/gin-gonic/gin"
    17  	"github.com/gin-gonic/gin/binding"
    18  	"github.com/go-playground/validator/v10"
    19  	"github.com/nais/knorten/pkg/database/gensql"
    20  )
    21  
    22  type teamForm struct {
    23  	Slug            string   `form:"team" binding:"required,validTeamName"`
    24  	Users           []string `form:"users[]" binding:"validEmail,userListNotEmpty"`
    25  	EnableAllowList string   `form:"enableallowlist"`
    26  	APIAccess       string   `form:"apiaccess"`
    27  }
    28  
    29  func formToTeam(ctx *gin.Context) (gensql.Team, error) {
    30  	var form teamForm
    31  	err := ctx.ShouldBindWith(&form, binding.Form)
    32  	if err != nil {
    33  		return gensql.Team{}, err
    34  	}
    35  
    36  	id, err := createTeamID(form.Slug)
    37  	if err != nil {
    38  		return gensql.Team{}, err
    39  	}
    40  
    41  	return gensql.Team{
    42  		ID:              id,
    43  		Slug:            form.Slug,
    44  		Users:           form.Users,
    45  		EnableAllowlist: form.EnableAllowList == "on",
    46  	}, nil
    47  }
    48  
    49  func (c *client) setupTeamRoutes() {
    50  	if v, ok := binding.Validator.Engine().(*validator.Validate); ok {
    51  		err := v.RegisterValidation("validEmail", ValidateUserEmails)
    52  		if err != nil {
    53  			c.log.WithError(err).Error("can't register validator")
    54  			return
    55  		}
    56  
    57  		err = v.RegisterValidation("userListNotEmpty", ValidateTeamUsers)
    58  		if err != nil {
    59  			c.log.WithError(err).Error("can't register validator")
    60  			return
    61  		}
    62  
    63  		err = v.RegisterValidation("validTeamName", ValidateTeamName)
    64  		if err != nil {
    65  			c.log.WithError(err).Error("can't register validator")
    66  			return
    67  		}
    68  	}
    69  
    70  	c.router.GET("/team/new", func(ctx *gin.Context) {
    71  		var form teamForm
    72  		session := sessions.Default(ctx)
    73  		flashes := session.Flashes()
    74  		err := session.Save()
    75  		if err != nil {
    76  			c.log.WithError(err).Error("problem saving session")
    77  			return
    78  		}
    79  
    80  		user, err := getUser(ctx)
    81  		if err != nil {
    82  			c.log.Error(err)
    83  			ctx.AbortWithStatusJSON(http.StatusUnauthorized, gin.H{"message": "unable to identify logged in user when creating team"})
    84  			return
    85  		}
    86  
    87  		form.Users = []string{user.Email}
    88  		c.htmlResponseWrapper(ctx, http.StatusOK, "team/new", gin.H{
    89  			"form":   form,
    90  			"errors": flashes,
    91  		})
    92  	})
    93  
    94  	c.router.POST("/team/new", func(ctx *gin.Context) {
    95  		err := c.newTeam(ctx)
    96  		if err != nil {
    97  			c.log.WithError(err).Info("create team")
    98  
    99  			session := sessions.Default(ctx)
   100  			var validationErrorse validator.ValidationErrors
   101  			if errors.As(err, &validationErrorse) {
   102  				for _, fieldError := range validationErrorse {
   103  					session.AddFlash(descriptiveMessageForTeamError(fieldError))
   104  				}
   105  			} else {
   106  				session.AddFlash(err.Error())
   107  			}
   108  			err = session.Save()
   109  			if err != nil {
   110  				c.log.WithError(err).Error("problem saving session")
   111  				return
   112  			}
   113  			ctx.Redirect(http.StatusSeeOther, "/team/new")
   114  			return
   115  		}
   116  		ctx.Redirect(http.StatusSeeOther, "/oversikt")
   117  	})
   118  
   119  	c.router.GET("/team/:slug/edit", func(ctx *gin.Context) {
   120  		teamSlug := ctx.Param("slug")
   121  		team, err := c.repo.TeamBySlugGet(ctx, teamSlug)
   122  		if err != nil {
   123  			if errors.Is(err, sql.ErrNoRows) {
   124  				ctx.JSON(http.StatusNotFound, map[string]string{
   125  					"status":  strconv.Itoa(http.StatusNotFound),
   126  					"message": fmt.Sprintf("team %v does not exist", teamSlug),
   127  				})
   128  				return
   129  			}
   130  			c.log.WithError(err).Errorf("problem getting team %v", teamSlug)
   131  			ctx.Redirect(http.StatusSeeOther, "/oversikt")
   132  			return
   133  		}
   134  
   135  		session := sessions.Default(ctx)
   136  		flashes := session.Flashes()
   137  		err = session.Save()
   138  		if err != nil {
   139  			c.log.WithError(err).Error("problem saving session")
   140  			return
   141  		}
   142  
   143  		c.htmlResponseWrapper(ctx, http.StatusOK, "team/edit", gin.H{
   144  			"team":            team,
   145  			"enableallowlist": team.EnableAllowlist,
   146  			"errors":          flashes,
   147  		})
   148  	})
   149  
   150  	c.router.POST("/team/:slug/edit", func(ctx *gin.Context) {
   151  		err := c.editTeam(ctx)
   152  		if err != nil {
   153  			c.log.WithError(err).Info("update team")
   154  			session := sessions.Default(ctx)
   155  			session.AddFlash(err.Error())
   156  			err := session.Save()
   157  			if err != nil {
   158  				c.log.WithError(err).Error("problem saving session")
   159  				return
   160  			}
   161  
   162  			teamSlug := ctx.Param("slug")
   163  			ctx.Redirect(http.StatusSeeOther, fmt.Sprintf("/team/%v/edit", teamSlug))
   164  			return
   165  		}
   166  		ctx.Redirect(http.StatusSeeOther, "/oversikt")
   167  	})
   168  
   169  	c.router.POST("/team/:slug/delete", func(ctx *gin.Context) {
   170  		teamSlug := ctx.Param("slug")
   171  		err := c.deleteTeam(ctx, teamSlug)
   172  		if err != nil {
   173  			session := sessions.Default(ctx)
   174  			session.AddFlash(err.Error())
   175  			err := session.Save()
   176  			if err != nil {
   177  				c.log.WithError(err).Error("problem saving session")
   178  				return
   179  			}
   180  			ctx.Redirect(http.StatusSeeOther, "/oversikt")
   181  			return
   182  		}
   183  		ctx.Redirect(http.StatusSeeOther, "/oversikt")
   184  	})
   185  
   186  	c.router.GET("/team/:slug/events", func(ctx *gin.Context) {
   187  		teamSlug := ctx.Param("slug")
   188  		team, err := c.repo.TeamBySlugGet(ctx, teamSlug)
   189  		if err != nil {
   190  			if errors.Is(err, sql.ErrNoRows) {
   191  				ctx.JSON(http.StatusNotFound, map[string]string{
   192  					"status":  strconv.Itoa(http.StatusNotFound),
   193  					"message": fmt.Sprintf("team %v does not exist", teamSlug),
   194  				})
   195  				return
   196  			}
   197  			c.log.WithError(err).Errorf("problem getting team %v", teamSlug)
   198  			ctx.Redirect(http.StatusSeeOther, "/oversikt")
   199  			return
   200  		}
   201  
   202  		events, err := c.repo.EventLogsForOwnerGet(ctx, team.ID, -1)
   203  		if err != nil {
   204  			return
   205  		}
   206  
   207  		session := sessions.Default(ctx)
   208  		flashes := session.Flashes()
   209  		err = session.Save()
   210  		if err != nil {
   211  			c.log.WithError(err).Error("problem saving session")
   212  			return
   213  		}
   214  
   215  		c.htmlResponseWrapper(ctx, http.StatusOK, "team/events", gin.H{
   216  			"events": events,
   217  			"slug":   team.Slug,
   218  			"errors": flashes,
   219  		})
   220  	})
   221  }
   222  
   223  func descriptiveMessageForTeamError(fieldError validator.FieldError) string {
   224  	switch fieldError.Tag() {
   225  	case "required":
   226  		field := fieldError.Field()
   227  		if field == "Slug" {
   228  			field = "Teamnavn"
   229  		}
   230  
   231  		return fmt.Sprintf("%v er et påkrevd felt", field)
   232  	case "validEmail":
   233  		return fmt.Sprintf("'%v' er ikke en godkjent NAV-bruker", fieldError.Value())
   234  	case "validTeamName":
   235  		return "Teamnavn må være med små bokstaver og bindestrek"
   236  	default:
   237  		return fieldError.Error()
   238  	}
   239  }
   240  
   241  var ValidateTeamName validator.Func = func(fl validator.FieldLevel) bool {
   242  	teamSlug := fl.Field().Interface().(string)
   243  
   244  	r, _ := regexp.Compile("^[a-z-]+$")
   245  	return r.MatchString(teamSlug)
   246  }
   247  
   248  var ValidateTeamUsers validator.Func = func(fl validator.FieldLevel) bool {
   249  	users, ok := fl.Field().Interface().([]string)
   250  	if !ok {
   251  		return false
   252  	}
   253  
   254  	return len(users) != 0
   255  }
   256  
   257  var ValidateUserEmails validator.Func = func(fl validator.FieldLevel) bool {
   258  	users, ok := fl.Field().Interface().([]string)
   259  	if !ok {
   260  		return false
   261  	}
   262  
   263  	for _, user := range users {
   264  		if user == "" {
   265  			continue
   266  		}
   267  		_, err := mail.ParseAddress(user)
   268  		if err != nil {
   269  			return false
   270  		}
   271  		if !strings.HasSuffix(strings.ToLower(user), "nav.no") {
   272  			return false
   273  		}
   274  	}
   275  
   276  	return true
   277  }
   278  
   279  func createTeamID(slug string) (string, error) {
   280  	if len(slug) > 25 {
   281  		slug = slug[:25]
   282  	}
   283  
   284  	randomBytes := make([]byte, 2)
   285  	_, err := rand.Read(randomBytes)
   286  	if err != nil {
   287  		return "", err
   288  	}
   289  
   290  	return slug + "-" + hex.EncodeToString(randomBytes), nil
   291  }
   292  
   293  func (c *client) newTeam(ctx *gin.Context) error {
   294  	team, err := formToTeam(ctx)
   295  	if err != nil {
   296  		return err
   297  	}
   298  
   299  	_, err = c.repo.TeamBySlugGet(ctx, team.Slug)
   300  	if err == nil {
   301  		return fmt.Errorf("team %v already exists", team.Slug)
   302  	}
   303  	if err != nil && !errors.Is(err, sql.ErrNoRows) {
   304  		return err
   305  	}
   306  
   307  	team.Users = removeEmptySliceElements(team.Users)
   308  	err = c.ensureUsersExists(team.Users)
   309  	if err != nil {
   310  		return err
   311  	}
   312  
   313  	return c.repo.RegisterCreateTeamEvent(ctx, team)
   314  }
   315  
   316  func (c *client) editTeam(ctx *gin.Context) error {
   317  	team, err := formToTeam(ctx)
   318  	if err != nil {
   319  		return err
   320  	}
   321  
   322  	existingTeam, err := c.repo.TeamBySlugGet(ctx, team.Slug)
   323  	if err != nil {
   324  		return err
   325  	}
   326  
   327  	team.ID = existingTeam.ID
   328  	team.Users = removeEmptySliceElements(team.Users)
   329  	return c.repo.RegisterUpdateTeamEvent(ctx, team)
   330  }
   331  
   332  func (c *client) ensureUsersExists(users []string) error {
   333  	for _, u := range users {
   334  		if err := c.azureClient.UserExistsInAzureAD(u); err != nil {
   335  			return err
   336  		}
   337  	}
   338  
   339  	return nil
   340  }
   341  
   342  func (c *client) deleteTeam(ctx *gin.Context, teamSlug string) error {
   343  	team, err := c.repo.TeamBySlugGet(ctx, teamSlug)
   344  	if err != nil {
   345  		return err
   346  	}
   347  
   348  	return c.repo.RegisterDeleteTeamEvent(ctx, team.ID)
   349  }