blob: 1dc70b54a803c216cda749905a6ca1c96f2faef9 [file] [log] [blame]
// Copyright 2015 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 coordinator
import (
"context"
"fmt"
"go.chromium.org/luci/auth/identity"
log "go.chromium.org/luci/common/logging"
cfglib "go.chromium.org/luci/config"
"go.chromium.org/luci/gae/service/datastore"
"go.chromium.org/luci/gae/service/info"
"go.chromium.org/luci/grpc/grpcutil"
"go.chromium.org/luci/logdog/api/config/svcconfig"
"go.chromium.org/luci/logdog/common/types"
"go.chromium.org/luci/logdog/server/config"
"go.chromium.org/luci/server/auth"
"google.golang.org/grpc/codes"
)
// NamespaceAccessType specifies the type of namespace access that is being
// requested for WithProjectNamespace.
type NamespaceAccessType int
const (
// NamespaceAccessNoAuth grants unconditional access to a project's namespace.
// This bypasses all ACL checks, and must only be used by service endpoints
// that explicitly apply ACLs elsewhere.
NamespaceAccessNoAuth NamespaceAccessType = iota
// NamespaceAccessAllTesting is an extension of NamespaceAccessNoAuth that,
// in addition to doing no ACL checks, also does no project existence checks.
//
// This must ONLY be used for testing.
NamespaceAccessAllTesting
// NamespaceAccessREAD enforces READ permission access to a project's
// namespace.
NamespaceAccessREAD
// NamespaceAccessWRITE enforces WRITE permission access to a project's
// namespace.
NamespaceAccessWRITE
)
// WithStream performs all ACL checks on a stream, including:
//
// - Project Namespace check
// - Logs purged check
//
// This returns the stream metadata, and a new context tied to the project.
// The new context can be used to fetch the LogStreamState from the datastore.
// The returned error is a gRPC error, parsable with grpcutil.
func WithStream(c context.Context, project string, path types.StreamPath, at NamespaceAccessType) (context.Context, *LogStream, error) {
nc := c
// Do the project namespace check. If successful, the namespace is installed
// into the resulting context.
if err := WithProjectNamespace(&nc, project, at); err != nil {
return c, nil, err
}
// Check to see if the log is purged.
ls := &LogStream{ID: LogStreamID(path)}
switch err := datastore.Get(nc, ls); {
case datastore.IsErrNoSuchEntity(err):
err = ErrPathNotFound
fallthrough
case err != nil:
return c, nil, err
}
// If this log entry is Purged and we're not admin, pretend it doesn't exist.
if ls.Purged {
if authErr := IsAdminUser(c); authErr != nil {
return c, nil, ErrPathNotFound
}
}
// Everything is fine.
return nc, ls, nil
}
// WithProjectNamespace sets the current namespace to the project name.
//
// It will return a user-facing wrapped gRPC error on failure:
// - InvalidArgument if the project name is invalid.
// - If the project exists, then
// - nil, if the user has the requested access.
// - Unauthenticated if the user does not have the requested access, but is
// also not authenticated. This lets them know they should try again after
// authenticating.
// - PermissionDenied if the user does not have the requested access.
// - PermissionDenied if the project doesn't exist.
// - Internal if an internal error occurred.
func WithProjectNamespace(c *context.Context, project string, at NamespaceAccessType) error {
ctx := *c
if err := cfglib.ValidateProjectName(project); err != nil {
log.WithError(err).Errorf(ctx, "Project name is invalid.")
return grpcutil.Errf(codes.InvalidArgument, "Project name is invalid: %s", err)
}
// Return gRPC error for when the user is denied access and does not have READ
// access. Returns either Unauthenticated if the user is not authenticated
// or PermissionDenied if the user is authenticated.
getAccessDeniedError := func() error {
if id := auth.CurrentIdentity(ctx); id.Kind() == identity.Anonymous {
return grpcutil.Unauthenticated
}
// Deny the existence of the project.
return grpcutil.Errf(codes.PermissionDenied,
"The project is invalid, or you do not have permission to access it.")
}
// Returns the project config, or "read denied" error if the project does not
// exist.
getProjectConfig := func() (*svcconfig.ProjectConfig, error) {
pcfg, err := config.ProjectConfig(ctx, project)
switch err {
case nil:
// Successfully loaded project config.
return pcfg, nil
case cfglib.ErrNoConfig, config.ErrInvalidConfig:
// If the configuration request was valid, but no configuration could be
// loaded, treat this as the user not having READ access to the project.
// Otherwise, the user could use this error response to confirm a
// project's existence.
log.Fields{
log.ErrorKey: err,
"project": project,
}.Errorf(ctx, "Could not load config for project.")
return nil, getAccessDeniedError()
default:
// The configuration attempt failed to load. This is an internal error,
// and is safe to return because it's not contingent on the existence (or
// lack thereof) of the project.
return nil, grpcutil.Internal
}
}
// Validate that the current user has the requested access.
switch at {
case NamespaceAccessNoAuth:
// Assert that the project exists and has a configuration.
if _, err := getProjectConfig(); err != nil {
return err
}
case NamespaceAccessAllTesting:
// Sanity check: this should only be used on development instances.
if !info.IsDevAppServer(ctx) {
panic("Testing access requested on non-development instance.")
}
break
case NamespaceAccessREAD:
// Assert that the current user has READ access.
pcfg, err := getProjectConfig()
if err != nil {
return err
}
if err := IsProjectReader(*c, pcfg); err != nil {
log.WithError(err).Warningf(*c, "User denied READ access to requested project.")
return getAccessDeniedError()
}
case NamespaceAccessWRITE:
// Assert that the current user has WRITE access.
pcfg, err := getProjectConfig()
if err != nil {
return err
}
if err := IsProjectWriter(*c, pcfg); err != nil {
log.WithError(err).Errorf(*c, "User denied WRITE access to requested project.")
return getAccessDeniedError()
}
default:
panic(fmt.Errorf("unknown access type: %v", at))
}
pns := ProjectNamespace(project)
nc, err := info.Namespace(ctx, pns)
if err != nil {
log.Fields{
log.ErrorKey: err,
"project": project,
"namespace": pns,
}.Errorf(ctx, "Failed to set namespace.")
return grpcutil.Internal
}
*c = nc
return nil
}
// Project returns the current project installed in the supplied Context's
// namespace.
//
// This function is called with the expectation that the Context is in a
// namespace conforming to ProjectNamespace. If this is not the case, this
// method will panic.
func Project(c context.Context) string {
ns := info.GetNamespace(c)
project := ProjectFromNamespace(ns)
if project != "" {
return project
}
panic(fmt.Errorf("current namespace %q does not begin with project namespace prefix (%q)", ns, ProjectNamespacePrefix))
}