blob: 64a4cf6c43e7a98a8f58f09ccc227dea90b94acb [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package handlers
import (
"bytes"
"encoding/json"
"infra/appengine/rotang"
"infra/appengine/rotang/pkg/algo"
"net/http"
"time"
"context"
"go.chromium.org/luci/server/auth"
"go.chromium.org/luci/server/router"
"go.chromium.org/luci/server/templates"
"google.golang.org/appengine"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
aeuser "google.golang.org/appengine/user"
)
var mtvTime = func() *time.Location {
loc, err := time.LoadLocation("America/Los_Angeles")
if err != nil {
panic(err)
}
return loc
}()
var isAdminForTest = false
func setAdminForTest(a bool) {
isAdminForTest = a
}
func isAdmin(ctx *router.Context) bool {
return isAdminForTest || aeuser.IsAdmin(appengine.NewContext(ctx.Request))
}
func adminOrOwner(ctx *router.Context, cfg *rotang.Configuration) bool {
usr := auth.CurrentUser(ctx.Context)
if usr == nil || usr.Email == "" {
return false
}
if isAdmin(ctx) {
return true
}
for _, m := range cfg.Config.Owners {
if usr.Email == m {
return true
}
}
return false
}
func isOwner(ctx context.Context, cfg *rotang.Configuration) bool {
usr := auth.CurrentUser(ctx)
if usr == nil || usr.Email == "" {
return false
}
for _, m := range cfg.Config.Owners {
if usr.Email == m {
return true
}
}
return false
}
// listRotations generates a list of rotations owned by current user.
// If the current user is an admin all rotations will be listed.
func (h *State) listRotations(ctx *router.Context, external bool) (templates.Args, error) {
if err := ctx.Context.Err(); err != nil {
return nil, err
}
usr := auth.CurrentUser(ctx.Context)
if usr == nil || usr.Email == "" {
return nil, status.Errorf(codes.PermissionDenied, "not logged in")
}
rotas, err := h.configStore(ctx.Context).RotaConfig(ctx.Context, "")
if err != nil && status.Code(err) != codes.NotFound {
return nil, err
}
if !external {
var noExternal []*rotang.Configuration
for _, r := range rotas {
if r.Config.External {
continue
}
noExternal = append(noExternal, r)
}
rotas = noExternal
}
if !isAdmin(ctx) {
var permRotas []*rotang.Configuration
for _, rota := range rotas {
for _, m := range rota.Config.Owners {
if usr.Email == m {
permRotas = append(permRotas, rota)
}
}
}
rotas = permRotas
}
return templates.Args{"Rotas": rotas}, nil
}
// modifyRotations generates the configuration and generators list used by the
// rota-create element.
func (h *State) modifyRotation(ctx *router.Context) (templates.Args, error) {
rotaName := ctx.Request.FormValue("name")
if rotaName == "" {
return nil, status.Errorf(codes.InvalidArgument, "`name` not set")
}
rotas, err := h.configStore(ctx.Context).RotaConfig(ctx.Context, rotaName)
if err != nil {
return nil, err
}
if len(rotas) != 1 {
return nil, status.Errorf(codes.Internal, "unexpected number of rotations returned")
}
rota := rotas[0]
if !adminOrOwner(ctx, rota) {
return nil, status.Errorf(codes.PermissionDenied, "not in the rotation owners")
}
var members []jsonMember
ms := h.memberStore(ctx.Context)
for _, rm := range rota.Members {
m, err := ms.Member(ctx.Context, rm.Email)
if err != nil {
return nil, err
}
members = append(members, jsonMember{
Name: m.Name,
Email: m.Email,
TZ: m.TZ.String(),
})
}
var resBuf bytes.Buffer
if err := json.NewEncoder(&resBuf).Encode(&jsonRota{
Cfg: *rota,
Members: members,
}); err != nil {
return nil, err
}
var genBuf bytes.Buffer
if err := json.NewEncoder(&genBuf).Encode(h.generators.List()); err != nil {
return nil, err
}
var modBuf bytes.Buffer
if err := json.NewEncoder(&modBuf).Encode(h.generators.ListModifiers()); err != nil {
return nil, err
}
return templates.Args{"Rota": rotaName, "Config": rota, "ConfigJSON": resBuf.String(), "Generators": genBuf.String(), "Modifiers": modBuf.String()}, nil
}
// rota authenticates the request and fetches the rotation configuration.
func (h *State) rota(ctx *router.Context) (*rotang.Configuration, error) {
if err := ctx.Context.Err(); err != nil {
return nil, err
}
rotaName := ctx.Request.FormValue("name")
if rotaName == "" {
return nil, status.Errorf(codes.InvalidArgument, "no rota provided")
}
rota, err := h.configStore(ctx.Context).RotaConfig(ctx.Context, rotaName)
if err != nil {
return nil, err
}
if len(rota) != 1 {
return nil, status.Errorf(codes.Internal, "expected only one rota to be returned")
}
if !adminOrOwner(ctx, rota[0]) {
return nil, status.Errorf(codes.PermissionDenied, "not rotation owner")
}
return rota[0], nil
}
func buildLegacyMap(h *State) map[string]func(ctx *router.Context, file string) (string, error) {
return map[string]func(ctx *router.Context, file string) (string, error){
// Trooper files.
"trooper.json": h.legacyTrooper,
"current_trooper.txt": h.legacyTrooper,
// Sheriff files.
"sheriff.js": h.buildSheriff,
"sheriff_gpu.js": h.legacySheriff,
"sheriff_android.js": h.androidSheriff,
"sheriff_ios.js": h.legacySheriff,
"sheriff.json": h.buildSheriff,
"sheriff_perf.json": h.legacySheriff,
"sheriff_gpu.json": h.legacySheriff,
"sheriff_angle.json": h.legacySheriff,
"sheriff_ios.json": h.legacySheriff,
"sheriff_perfbot.json": h.legacySheriff,
"sheriff_flutter_engine.json": h.legacySheriff,
// All rotations
"all_rotations.js": h.legacyAllRotations,
}
}
// Email defines the data needed to send out
// on call emails.
type Email struct {
Subject string
Body string
}
// State holds shared state between handlers.
type State struct {
projectID func(context.Context) string
prodENV string
calendar rotang.Calenderer
legacyCalendar rotang.Calenderer
generators *algo.Generators
backupCred func(*router.Context) (*http.Client, error)
memberStore func(context.Context) rotang.MemberStorer
shiftStore func(context.Context) rotang.ShiftStorer
configStore func(context.Context) rotang.ConfigStorer
mailAddress string
authGroup string
missingEmail Email
declinedEventEmail Email
mailSender rotang.MailSender
legacyMap map[string]func(ctx *router.Context, file string) (string, error)
}
// Options contains the options used by the handlers.
type Options struct {
ProjectID func(context.Context) string
ProdENV string
Calendar rotang.Calenderer
LegacyCalendar rotang.Calenderer
Generators *algo.Generators
MailSender rotang.MailSender
MailAddress string
MissingEmail Email
DeclinedEventEmail Email
BackupCred func(*router.Context) (*http.Client, error)
AuthGroup string
MemberStore func(context.Context) rotang.MemberStorer
ConfigStore func(context.Context) rotang.ConfigStorer
ShiftStore func(context.Context) rotang.ShiftStorer
}
// New creates a new handlers State container.
func New(opt *Options) (*State, error) {
switch {
case opt == nil:
return nil, status.Errorf(codes.InvalidArgument, "opt can not be nil")
case opt.ProdENV == "":
return nil, status.Errorf(codes.InvalidArgument, "ProdENV must be set")
case opt.Calendar == nil:
return nil, status.Errorf(codes.InvalidArgument, "Calendar can not be nil")
case opt.LegacyCalendar == nil:
return nil, status.Errorf(codes.InvalidArgument, "LegacyCalendar can not be nil")
case opt.Generators == nil:
return nil, status.Errorf(codes.InvalidArgument, "Genarators can not be nil")
case opt.MemberStore == nil, opt.ShiftStore == nil, opt.ConfigStore == nil:
return nil, status.Errorf(codes.InvalidArgument, "Store functions can not be nil")
case opt.BackupCred == nil:
return nil, status.Errorf(codes.InvalidArgument, "BackupCred can not be nil")
case opt.ProjectID == nil:
return nil, status.Errorf(codes.InvalidArgument, "ProjectID can not be nil")
case opt.MissingEmail.Subject == "" || opt.MissingEmail.Body == "":
return nil, status.Errorf(codes.InvalidArgument, "MissingEmail can not be empty")
case opt.DeclinedEventEmail.Subject == "" || opt.DeclinedEventEmail.Body == "":
return nil, status.Errorf(codes.InvalidArgument, "DeclinedEventEmail can not be empty")
}
h := &State{
prodENV: opt.ProdENV,
projectID: opt.ProjectID,
calendar: opt.Calendar,
legacyCalendar: opt.LegacyCalendar,
generators: opt.Generators,
memberStore: opt.MemberStore,
shiftStore: opt.ShiftStore,
configStore: opt.ConfigStore,
mailSender: opt.MailSender,
mailAddress: opt.MailAddress,
missingEmail: opt.MissingEmail,
declinedEventEmail: opt.DeclinedEventEmail,
backupCred: opt.BackupCred,
authGroup: opt.AuthGroup,
}
h.legacyMap = buildLegacyMap(h)
return h, nil
}
// IsProduction is true if the service is running in production.
func (h *State) IsProduction() bool {
return h.prodENV == "production"
}
// IsStaging is true if the service is running in staging.
func (h *State) IsStaging() bool {
return h.prodENV == "staging"
}
// IsLocal is true if the service is running in the local dev environment.
func (h *State) IsLocal() bool {
return h.prodENV == "local"
}