blob: c608483f72cba302912c24a6b5e13a1c952579ba [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"
"google.golang.org/grpc/codes"
"go.chromium.org/luci/auth/identity"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/grpc/grpcutil"
"go.chromium.org/luci/logdog/common/types"
"go.chromium.org/luci/logdog/server/config"
"go.chromium.org/luci/server/auth"
"go.chromium.org/luci/server/auth/realms"
)
var (
// PermLogsCreate is a permission required for RegisterPrefix RPC.
PermLogsCreate = realms.RegisterPermission("logdog.logs.create")
// PermLogsGet is a permission required for reading individual streams.
PermLogsGet = realms.RegisterPermission("logdog.logs.get")
// PermLogsList is a permission required for listing streams in a prefix.
PermLogsList = realms.RegisterPermission("logdog.logs.list")
)
// PermissionDeniedErr is a generic "doesn't exist or don't have access" error.
//
// If the request is anonymous, it is an Unauthenticated error instead.
func PermissionDeniedErr(ctx context.Context) error {
if id := auth.CurrentIdentity(ctx); id.Kind() == identity.Anonymous {
return grpcutil.Unauthenticated
}
return grpcutil.Errf(codes.PermissionDenied,
"The resource doesn't exist or you do not have permission to access it.")
}
// CheckPermission checks the caller has the requested permission.
//
// `realm` can be an empty string when accessing older LogPrefix entities not
// associated with any realms or when RegisterPrefix is called without a realm.
//
// If the project has `enforce_realms_in` setting ON in the "@root" realm, will
// use realms ACLs exclusively. Otherwise the overall ACL is a union of the
// realms ACLs and legacy ACLs. Fallbacks to legacy ACLs are logged.
//
// Logs the outcome inside (`prefix` is used only in this logging). Returns
// gRPC errors that can be returned directly to the caller.
func CheckPermission(ctx context.Context, perm realms.Permission, prefix types.StreamName, realm string) error {
// Check no cross-project mix up is happening as a precaution.
project := Project(ctx)
if realm != "" {
if projInRealm, _ := realms.Split(realm); projInRealm != project {
logging.Errorf(ctx, "Unexpectedly checking realm %q in a context of project %q", realm, project)
return grpcutil.Internal
}
}
// The project as a whole is either in "allow realms, but fallback to old
// ACLs" or in "enforce realms" modes, depending on configuration stored in
// the @root realm. We can't just check `realm` here because it can be an
// empty string.
enforce, err := auth.ShouldEnforceRealmACL(ctx, realms.Join(project, realms.RootRealm))
if err != nil {
logging.WithError(err).Errorf(ctx, "failed to check realms ACL enforcement")
return grpcutil.Internal
}
// When registering a new prefix a realm is required if in the "enforce" mode.
// Otherwise the missing realm is substituted with "@legacy". That way old
// LogPrefix entities without "realm" field get some read ACLs, and legacy
// RegisterPrefix callers that omit "realm" field still hit some realm ACLs
// before hitting the legacy ones.
if realm == "" {
if perm == PermLogsCreate && enforce {
logging.Fields{
"identity": auth.CurrentIdentity(ctx),
"prefix": prefix,
"project": project,
}.Errorf(ctx, "Missing `realm` in the request")
return grpcutil.Errf(codes.InvalidArgument, "a realm is required when registering a prefix in project %q", project)
}
realm = realms.Join(project, realms.LegacyRealm)
}
// Log all the details to help debug permission issues.
ctx = logging.SetFields(ctx, logging.Fields{
"identity": auth.CurrentIdentity(ctx),
"perm": perm,
"prefix": prefix,
"realm": realm,
})
// Do the realms ACL check.
granted, err := auth.HasPermission(ctx, perm, realm)
if err != nil {
logging.WithError(err).Errorf(ctx, "failed to check realms ACL")
return grpcutil.Internal
}
// If in the enforcement mode, this is all we do. Don't use legacy ACLs.
// Also if got a positive result, no need to check legacy ACLs at all.
if enforce || granted {
if granted {
logging.Debugf(ctx, "Permission granted")
return nil
}
logging.Warningf(ctx, "Permission denied")
return PermissionDeniedErr(ctx)
}
// On a negative outcome fallback to legacy ACLs, so nothing breaks.
switch perm {
case PermLogsCreate:
granted, err = checkLegacyProjectWriter(ctx)
case PermLogsGet, PermLogsList:
granted, err = checkLegacyProjectReader(ctx)
default:
panic(fmt.Sprintf("CheckPermission got unexpected permissions %q", perm))
}
if err != nil {
return grpcutil.Internal
}
// Log if the permission is granted through the legacy ACL to be able to
// eventually verify legacy ACLs can be removed. Note that the outcome of
// the legacy ACL check was already logged by the legacy ACL implementation.
if granted {
logging.Warningf(ctx, "crbug.com/1172492: triggered fallback to legacy ACLs")
return nil
}
return PermissionDeniedErr(ctx)
}
// CheckAdminUser tests whether the current user belongs to the administrative
// users group.
//
// Logs the outcome inside. The error is non-nil only if the check itself fails.
func CheckAdminUser(ctx context.Context) (bool, error) {
cfg, err := config.Config(ctx)
if err != nil {
logging.WithError(err).Errorf(ctx, "Failed to load service config")
return false, err
}
return checkMember(ctx, "ADMIN", cfg.Coordinator.AdminAuthGroup)
}
// CheckServiceUser tests whether the current user belongs to the backend
// services users group.
//
// Logs the outcome inside. The error is non-nil only if the check itself fails.
func CheckServiceUser(ctx context.Context) (bool, error) {
cfg, err := config.Config(ctx)
if err != nil {
logging.WithError(err).Errorf(ctx, "Failed to load service config")
return false, err
}
return checkMember(ctx, "SERVICE", cfg.Coordinator.ServiceAuthGroup)
}
// checkLegacyProjectReader tests whether the current user belongs to one of the
// current project's declared reader groups.
//
// Usable only when inside some project namespace, see WithProjectNamespace.
// Panics otherwise.
//
// Logs the outcome inside. The error is non-nil only if the check itself fails.
func checkLegacyProjectReader(ctx context.Context) (bool, error) {
pcfg, err := ProjectConfig(ctx)
if err != nil {
panic("checkLegacyProjectReader is called outside of a project namespace")
}
return checkMember(ctx, "READ", pcfg.ReaderAuthGroups...)
}
// checkLegacyProjectWriter tests whether the current user belongs to one of the
// current project's declared writer groups.
//
// Usable only when inside some project namespace, see WithProjectNamespace.
// Panics otherwise.
//
// Logs the outcome inside. The error is non-nil only if the check itself fails.
func checkLegacyProjectWriter(ctx context.Context) (bool, error) {
pcfg, err := ProjectConfig(ctx)
if err != nil {
panic("checkLegacyProjectWriter is called outside of a project namespace")
}
return checkMember(ctx, "WRITE", pcfg.WriterAuthGroups...)
}
func checkMember(ctx context.Context, action string, groups ...string) (bool, error) {
switch yes, err := auth.IsMember(ctx, groups...); {
case err != nil:
logging.Fields{
"identity": auth.CurrentIdentity(ctx),
"groups": groups,
logging.ErrorKey: err,
}.Errorf(ctx, "Membership check failed")
return false, err
case yes:
logging.Fields{
"identity": auth.CurrentIdentity(ctx),
"groups": groups,
}.Debugf(ctx, "User %s access granted.", action)
return true, nil
default:
logging.Fields{
"identity": auth.CurrentIdentity(ctx),
"groups": groups,
}.Warningf(ctx, "User %s access denied.", action)
return false, nil
}
}