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 }