blob: b806a03300daeeafa53d6af85bdefed413388b21 [file] [log] [blame]
// Copyright 2016 The LUCI Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package portal implements HTTP routes for portal pages.
//
// These pages can be registered at init()-time, and will be routed to
// /admin/portal.
//
// Typically they read/write `settings` as defined by
// `go.chromium.org/luci/server/settings`, but they can also be used to provide
// information to administrators or to provide admin-only actions (such as
// clearing queues or providing admin tokens).
package portal
import (
"context"
"html/template"
"net"
"net/http"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/common/retry/transient"
"go.chromium.org/luci/auth/identity"
"go.chromium.org/luci/server/auth"
"go.chromium.org/luci/server/auth/authdb"
"go.chromium.org/luci/server/auth/xsrf"
"go.chromium.org/luci/server/router"
"go.chromium.org/luci/server/templates"
"go.chromium.org/luci/server/portal/internal/assets"
)
// AssumeTrustedPort can be passed as auth.Method to InstallHandlers to indicate
// that portal endpoints are being exposed on an internal port accessible only
// to cluster administrators and no additional auth checks are required (or they
// are not possible).
var AssumeTrustedPort auth.Method = trustedPortAuth{}
// InstallHandlers installs HTTP handlers that implement admin UI.
//
// `adminAuth` is the method that will be used to authenticate the access
// (regardless of what's installed in the base context). It must be able to
// distinguish admins (aka superusers) from non-admins. It is needed because
// settings UI must be usable even before auth system is configured.
//
// `adminAuth` can be a special value portal.AssumeTrustedPort which completely
// disables all authentication and authorization checks (by delegating them to
// the network layer).
func InstallHandlers(r *router.Router, base router.MiddlewareChain, adminAuth auth.Method) {
tmpl := &templates.Bundle{
Loader: templates.AssetsLoader(assets.Assets()),
DefaultTemplate: "base",
FuncMap: template.FuncMap{
"includeCSS": func(name string) template.CSS { return template.CSS(assets.GetAsset(name)) },
"includeJS": func(name string) template.JS { return template.JS(assets.GetAsset(name)) },
},
DefaultArgs: func(c context.Context, e *templates.Extra) (templates.Args, error) {
logoutURL, err := auth.LogoutURL(c, "/")
if err != nil && err != auth.ErrNoUsersAPI {
return nil, err
}
return templates.Args{
"Email": auth.CurrentUser(c).Email,
"LogoutURL": logoutURL,
}, nil
},
}
adminDB := adminBypassDB{
authdb.ErroringDB{
Error: errors.New("admin: unexpected call to authdb.DB on admin page"),
},
}
rr := r.Subrouter("/admin/portal")
rr.Use(base.Extend(
templates.WithTemplates(tmpl),
adminDB.install,
auth.Authenticate(adminAuth),
adminAutologin,
))
rr.GET("", router.MiddlewareChain{}, indexPage)
rr.GET("/:PageKey", router.MiddlewareChain{}, portalPageGET)
rr.POST("/:PageKey", router.NewMiddlewareChain(xsrf.WithTokenCheck), portalPagePOST)
rr.POST("/:PageKey/:ActionID", router.NewMiddlewareChain(xsrf.WithTokenCheck), portalActionPOST)
}
// replyError sends HTML error page with status 500 on transient errors or 400
// on fatal ones.
func replyError(c context.Context, rw http.ResponseWriter, err error) {
if transient.Tag.In(err) {
rw.WriteHeader(http.StatusInternalServerError)
} else {
rw.WriteHeader(http.StatusBadRequest)
}
templates.MustRender(c, rw, "pages/error.html", templates.Args{
"Error": err.Error(),
})
}
////////////////////////////////////////////////////////////////////////////////
// Auth related helpers.
// trustedPortAuth is auth.Method that assumes all request are coming from a
// super-admin through a trusted internal port.
type trustedPortAuth struct{}
// Authenticate is part of auth.Method interface.
//
// It returns User with Anonymous identity that has Superuser bit set: we
// know *some* admin is accessing endpoints (thus we set Superuser bit), but
// don't know who they are exactly (thus setting Anonymous identity).
func (trustedPortAuth) Authenticate(context.Context, *http.Request) (*auth.User, error) {
return &auth.User{
Identity: identity.AnonymousIdentity,
Superuser: true,
}, nil
}
// adminBypassDB skips IP whitelist checks (assuming no IPs are whitelisted) and
// errors on all other checks.
//
// It is needed to make admin pages accessible even when AuthDB is not
// configured.
type adminBypassDB struct {
authdb.ErroringDB
}
func (adminBypassDB) GetWhitelistForIdentity(c context.Context, ident identity.Identity) (string, error) {
return "", nil
}
func (adminBypassDB) IsInWhitelist(c context.Context, ip net.IP, whitelist string) (bool, error) {
return false, nil
}
func (d adminBypassDB) install(c *router.Context, next router.Handler) {
c.Context = auth.ModifyConfig(c.Context, func(cfg auth.Config) auth.Config {
cfg.DBProvider = func(context.Context) (authdb.DB, error) {
return d, nil
}
return cfg
})
next(c)
}
// adminAutologin is middleware that ensures authenticated user is local site
// admin (aka superuser).
//
// On GAE it grants access only to users that have Editor or Owner roles in
// the Cloud Project.
//
// It redirect anonymous users to login page, and displays "Access denied" page
// to authenticated non-admin users.
func adminAutologin(c *router.Context, next router.Handler) {
u := auth.CurrentUser(c.Context)
// Redirect anonymous users to a login page that redirects back to the current
// page. Don't do it if this anonymous user is also marked as Superuser, which
// happens when using AssumeTrustedPort auth method.
if u.Identity == identity.AnonymousIdentity && !u.Superuser {
// Make the current URL relative to the host.
destURL := *c.Request.URL
destURL.Host = ""
destURL.Scheme = ""
url, err := auth.LoginURL(c.Context, destURL.String())
if err != nil {
logging.WithError(err).Errorf(c.Context, "Error when generating login URL")
if transient.Tag.In(err) {
http.Error(c.Writer, "Transient error when generating login URL, see logs", 500)
} else {
http.Error(c.Writer, "Can't generate login URL, see logs", 401)
}
return
}
http.Redirect(c.Writer, c.Request, url, 302)
return
}
// Only superusers can proceed.
if !u.Superuser {
c.Writer.WriteHeader(http.StatusForbidden)
templates.MustRender(c.Context, c.Writer, "pages/access_denied.html", nil)
return
}
next(c)
}