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 }