go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/auth/realms/realms.go (about)

     1  // Copyright 2020 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package realms
    16  
    17  import (
    18  	"fmt"
    19  	"regexp"
    20  	"strings"
    21  
    22  	"go.chromium.org/luci/common/errors"
    23  )
    24  
    25  var (
    26  	projectNameRe = regexp.MustCompile(`^[a-z0-9\-_]{1,100}$`)
    27  	realmNameRe   = regexp.MustCompile(`^[a-z0-9_\.\-/]{1,400}$`)
    28  )
    29  
    30  // RealmNameScope specifies how realm names are scoped for ValidateRealmName.
    31  type RealmNameScope string
    32  
    33  const (
    34  	// GlobalScope indicates the realm name is not scoped to a project.
    35  	//
    36  	// E.g. it is "<project>:<realm>".
    37  	GlobalScope RealmNameScope = "global"
    38  
    39  	// ProjectScope indicates the realm name is scoped to some project.
    40  	//
    41  	// E.g. it is just "<realm>" (instead of "<project>:<realm>").
    42  	ProjectScope RealmNameScope = "project-scoped"
    43  )
    44  
    45  const (
    46  	// InternalProject is an alias for "@internal".
    47  	//
    48  	// There's a special set of realms (called internal realms or, sometimes,
    49  	// global realms) that are defined in realms.cfg in the LUCI Auth service
    50  	// config set. They are not part of any particular LUCI project. Their full
    51  	// name have form "@internal:<realm>".
    52  	InternalProject = "@internal"
    53  
    54  	// RootRealm is an alias for "@root".
    55  	//
    56  	// The root realm is implicitly included into all other realms (including
    57  	// "@legacy"), and it is also used as a fallback when a resource points to
    58  	// a realm that no longer exists. Without the root realm, such resources
    59  	// become effectively inaccessible and this may be undesirable. Permissions in
    60  	// the root realm apply to all realms in the project (current, past and
    61  	// future), and thus the root realm should contain only administrative-level
    62  	// bindings.
    63  	//
    64  	// HasPermission() automatically falls back to corresponding root realms if
    65  	// any of the realms it receives do not exist. You still can pass a root realm
    66  	// to HasPermission() if you specifically want to check the root realm
    67  	// permissions.
    68  	RootRealm = "@root"
    69  
    70  	// LegacyRealm is an alias for "@legacy".
    71  	//
    72  	// The legacy realm should be used for legacy resources created before the
    73  	// realms mechanism was introduced in case the service can't figure out a more
    74  	// appropriate realm based on resource's properties. The service must clearly
    75  	// document when and how it uses the legacy realm (if it uses it at all).
    76  	//
    77  	// Unlike the situation with root realms, HasPermission() has no special
    78  	// handling of legacy realms. You should always pass them to HasPermission()
    79  	// explicitly when checking permissions of legacy resources.
    80  	LegacyRealm = "@legacy"
    81  
    82  	// ProjectRealm is an alias for "@project".
    83  	//
    84  	// The project realm is used to store realms-aware resources which are global
    85  	// to the entire project, for example the configuration of the project itself,
    86  	// or derevations thereof. The root realm is explicitly NOT recommended for
    87  	// this because there's no way to grant permissions in the root realm without
    88  	// also implicitly granting them in ALL other realms.
    89  	ProjectRealm = "@project"
    90  )
    91  
    92  // ValidateRealmName validates a realm name (either full or project-scoped).
    93  //
    94  // If `scope` is GlobalScope, `realm` is expected to have the form
    95  // "<project>:<realm>". If `scope` is ProjectScope, `realm` is expected to have
    96  // the form "<realm>". Any other values of `scope` cause panics.
    97  //
    98  // In either case "<realm>" is tested against `^[a-z0-9_\.\-/]{1,400}$` and
    99  // compared to literals "@root" and "@legacy".
   100  //
   101  // When validating globally scoped names, "<project>" is tested using
   102  // ValidateProjectName.
   103  func ValidateRealmName(realm string, scope RealmNameScope) error {
   104  	if scope == GlobalScope {
   105  		idx := strings.IndexRune(realm, ':')
   106  		if idx == -1 {
   107  			return errors.Reason("bad %s realm name %q - should be <project>:<realm>", scope, realm).Err()
   108  		}
   109  		if err := ValidateProjectName(realm[:idx]); err != nil {
   110  			return errors.Annotate(err, "bad %s realm name %q", scope, realm).Err()
   111  		}
   112  		realm = realm[idx+1:]
   113  	} else if scope != ProjectScope {
   114  		panic(fmt.Sprintf("invalid RealmNameScope %q", scope))
   115  	}
   116  
   117  	if realm != RootRealm && realm != LegacyRealm && realm != ProjectRealm && !realmNameRe.MatchString(realm) {
   118  		return errors.Reason("bad %s realm name %q - the realm name should match %q or be %q, %q or %q",
   119  			scope, realm, realmNameRe, RootRealm, LegacyRealm, ProjectRealm).Err()
   120  	}
   121  
   122  	return nil
   123  }
   124  
   125  // ValidateProjectName validates a project portion of a full realm name.
   126  //
   127  // It should match `^[a-z0-9\-_]{1,100}$` or be "@internal".
   128  func ValidateProjectName(project string) error {
   129  	// Note: we don't mention @internal in the error message intentionally.
   130  	// Internal realms are uncommon and mentioning them in a generic error message
   131  	// will just confuse users.
   132  	if project != InternalProject && !projectNameRe.MatchString(project) {
   133  		return errors.Reason("bad project name %q - should match %q", project, projectNameRe).Err()
   134  	}
   135  	return nil
   136  }
   137  
   138  // Split splits a global realm name "<project>:<realm>" into its components.
   139  //
   140  // Panics if `global` doesn't have ":". Doesn't validate the resulting
   141  // components. If this is a concern, use ValidateRealmName explicitly.
   142  func Split(global string) (project, realm string) {
   143  	idx := strings.IndexRune(global, ':')
   144  	if idx == -1 {
   145  		panic(fmt.Sprintf("bad realm name %q - should be <project>:<realm>", global))
   146  	}
   147  	return global[:idx], global[idx+1:]
   148  }
   149  
   150  // Join returns "<project>:<realm>".
   151  //
   152  // Doesn't validate the result. If this is a concern, use ValidateRealmName
   153  // explicitly.
   154  func Join(project, realm string) (global string) {
   155  	return project + ":" + realm
   156  }