go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/server/auth/realms/permissions.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  	"strings"
    19  	"sync"
    20  
    21  	"go.chromium.org/luci/common/errors"
    22  )
    23  
    24  var (
    25  	mu            sync.RWMutex
    26  	perms         = map[string]PermissionFlags{}
    27  	forbidChanges bool
    28  )
    29  
    30  // PermissionFlags describe how a registered permission is going to be used in
    31  // the current process.
    32  //
    33  // These are purely process-local flags. It is possible for the same single
    34  // permission to be defined with different flags in different processes.
    35  type PermissionFlags uint8
    36  
    37  const (
    38  	// UsedInQueryRealms indicates the permission will be used with QueryRealms.
    39  	//
    40  	// It instructs the runtime to build an in-memory index to speed up the query.
    41  	// This flag is necessary for QueryRealms function to work. It implies some
    42  	// performance and memory costs and should be used only with permissions that
    43  	// are going to be passed to QueryRealms.
    44  	UsedInQueryRealms PermissionFlags = 1 << iota
    45  )
    46  
    47  // Permission is a symbol that has form "<service>.<subject>.<verb>", which
    48  // describes some elementary action ("<verb>") that can be done to some category
    49  // of resources ("<subject>"), managed by some particular kind of LUCI service
    50  // ("<service>").
    51  //
    52  // Each individual LUCI service should document what permissions it checks and
    53  // when. It becomes a part of service's public API. Usually services should
    54  // check only permissions of resources they own (e.g. "<service>.<subject>.*"),
    55  // but in exceptional cases they may also check permissions intended for other
    56  // services. This is primarily useful for services that somehow "proxy" access
    57  // to resources.
    58  type Permission struct {
    59  	name string
    60  }
    61  
    62  // Name is "<service>.<subject>.<verb>" string.
    63  func (p Permission) Name() string {
    64  	return p.name
    65  }
    66  
    67  // String allows permissions to be used as "%s" in format strings.
    68  func (p Permission) String() string {
    69  	return p.name
    70  }
    71  
    72  // AddFlags appends given flags to a registered permission.
    73  //
    74  // Permissions are usually defined in shared packages used by multiple services.
    75  // Flags that makes sense for one service do not always make sense for another.
    76  // For that reason RegisterPermission and AddFlags are two separate functions.
    77  //
    78  // Must to be called during startup, e.g. in init(). Panics if called when
    79  // the process is already serving requests.
    80  func (p Permission) AddFlags(flags PermissionFlags) {
    81  	mu.Lock()
    82  	defer mu.Unlock()
    83  	if forbidChanges {
    84  		panic("cannot call Permission.AddFlags while already serving requests, do it before starting the serving loop, e.g. in init()")
    85  	}
    86  	perms[p.name] |= flags
    87  }
    88  
    89  // clearPermissions removes all registered permissions (for tests).
    90  func clearPermissions() {
    91  	mu.Lock()
    92  	perms = map[string]PermissionFlags{}
    93  	forbidChanges = false
    94  	mu.Unlock()
    95  }
    96  
    97  // RegisterPermission adds a new permission with the given name to the process
    98  // registry or returns an existing one.
    99  //
   100  // Panics if the permission name doesn't look like "<service>.<subject>.<verb>".
   101  //
   102  // Must to be called during startup, e.g. in init(). Panics if called when
   103  // the process is already serving requests.
   104  func RegisterPermission(name string) Permission {
   105  	mu.Lock()
   106  	defer mu.Unlock()
   107  	if forbidChanges {
   108  		panic("cannot call RegisterPermission while already serving requests, do it before starting the serving loop, e.g. in init()")
   109  	}
   110  	if err := ValidatePermissionName(name); err != nil {
   111  		panic(err)
   112  	}
   113  	perms[name] |= 0
   114  	return Permission{name: name}
   115  }
   116  
   117  // GetPermissions returns the permissions with the matching names. The order of
   118  // the permission is the same as the provided names.
   119  //
   120  // Implicitly calls ForbidPermissionChanges to make sure no new permissions
   121  // are added later.
   122  //
   123  // Returns an error if any of the permission isn't registered.
   124  func GetPermissions(names ...string) ([]Permission, error) {
   125  	mu.RLock()
   126  	var err error
   127  	for _, name := range names {
   128  		if _, ok := perms[name]; !ok {
   129  			err = errors.Reason("permission not registered: %q", name).Err()
   130  			break
   131  		}
   132  	}
   133  	forbiddenAlready := forbidChanges
   134  	mu.RUnlock()
   135  
   136  	if err != nil {
   137  		return nil, err
   138  	}
   139  
   140  	if !forbiddenAlready {
   141  		mu.Lock()
   142  		forbidChanges = true
   143  		mu.Unlock()
   144  	}
   145  
   146  	perms := make([]Permission, 0, len(names))
   147  	for _, name := range names {
   148  		perms = append(perms, Permission{name})
   149  	}
   150  	return perms, nil
   151  }
   152  
   153  // RegisteredPermissions returns a snapshot of all registered permissions along
   154  // with their flags.
   155  //
   156  // Implicitly calls ForbidPermissionChanges to make sure no new permissions
   157  // are added later.
   158  func RegisteredPermissions() map[Permission]PermissionFlags {
   159  	mu.RLock()
   160  	all := make(map[Permission]PermissionFlags, len(perms))
   161  	for name, flags := range perms {
   162  		all[Permission{name: name}] = flags
   163  	}
   164  	forbiddenAlready := forbidChanges
   165  	mu.RUnlock()
   166  
   167  	if !forbiddenAlready {
   168  		mu.Lock()
   169  		forbidChanges = true
   170  		mu.Unlock()
   171  	}
   172  
   173  	return all
   174  }
   175  
   176  // ForbidPermissionChanges explicitly forbids registering new permissions or
   177  // changing their flags.
   178  //
   179  // All permissions should be registered before the server starts running its
   180  // loop. The runtime relies on this when building various caches. After this
   181  // function is called, RegisterPermissions and AddFlags would start panicking.
   182  //
   183  // Intended for internal server code.
   184  func ForbidPermissionChanges() {
   185  	mu.Lock()
   186  	forbidChanges = true
   187  	mu.Unlock()
   188  }
   189  
   190  // ValidatePermissionName returns an error if the permission name is invalid.
   191  //
   192  // It checks the name looks like "<service>.<subject>.<verb>".
   193  func ValidatePermissionName(name string) error {
   194  	if parts := strings.Split(name, "."); len(parts) == 3 {
   195  		good := true
   196  		for _, p := range parts {
   197  			if p == "" {
   198  				good = false
   199  				break
   200  			}
   201  		}
   202  		if good {
   203  			return nil
   204  		}
   205  	}
   206  	return errors.Reason("bad permission %q - must have form <service>.<subject>.<verb>", name).Err()
   207  }