blob: 815d11d9c0af3dd763cc71af299ed5dbce9d8c47 [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
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// 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
// ``, 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 (
// 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{
Error: errors.New("admin: unexpected call to authdb.DB on admin page"),
rr := r.Subrouter("/admin/portal")
rr.GET("", nil, indexPage)
rr.GET("/:PageKey", nil, portalPageGET)
rr.POST("/:PageKey", router.NewMiddlewareChain(xsrf.WithTokenCheck), portalPagePOST)
rr.GET("/:PageKey/:ActionID", nil, portalActionGETPOST)
rr.POST("/:PageKey/:ActionID", router.NewMiddlewareChain(xsrf.WithTokenCheck), portalActionGETPOST)
// 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) {
} else {
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, auth.Session, error) {
return &auth.User{
Identity: identity.AnonymousIdentity,
Superuser: true,
}, nil, 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 {
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
// 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)
http.Redirect(c.Writer, c.Request, url, 302)
// Only superusers can proceed.
if !u.Superuser {
templates.MustRender(c.Context, c.Writer, "pages/access_denied.html", nil)