Add "billing_information" RBAC resource (#5676)

* Expose GRPC client connection to plugins
* Replaces global plugin state with the PluginRegistry
This commit is contained in:
Alexey Kontsevoy
2021-03-01 22:47:03 -05:00
committed by GitHub
parent 4f102552ee
commit 472df28f2a
18 changed files with 222 additions and 136 deletions
+5
View File
@@ -83,6 +83,11 @@ func New(ctx context.Context, cfg Config) (*Client, error) {
return c, nil
}
// GetConnection returns GRPC connection
func (c *Client) GetConnection() *grpc.ClientConn {
return c.conn
}
// getDialer builds a grpc dialer for the client from a ContextDialer.
// The ContextDialer is chosen from available options, preferring one from
// credentials, then from configuration, and lastly from addresses.
+3
View File
@@ -190,6 +190,9 @@ const (
// KindMFADevice is an MFA device for a user.
KindMFADevice = "mfa_device"
// KindBilling represents access to cloud billing features
KindBilling = "billing"
// V3 is the third version of resources.
V3 = "v3"
+1 -1
Submodule e updated: f8bf84cc52...0e18de8679
+9 -5
View File
@@ -1,5 +1,5 @@
/*
Copyright 2015-2019 Gravitational, Inc.
Copyright 2015-2021 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -35,6 +35,7 @@ import (
"github.com/gravitational/teleport/lib/defaults"
"github.com/gravitational/teleport/lib/events"
"github.com/gravitational/teleport/lib/httplib"
"github.com/gravitational/teleport/lib/plugin"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/session"
"github.com/gravitational/teleport/lib/utils"
@@ -46,6 +47,7 @@ import (
)
type APIConfig struct {
PluginRegistry plugin.Registry
AuthServer *Server
SessionService session.Service
AuditLog events.IAuditLog
@@ -80,7 +82,7 @@ type APIServer struct {
}
// NewAPIServer returns a new instance of APIServer HTTP handler
func NewAPIServer(config *APIConfig) http.Handler {
func NewAPIServer(config *APIConfig) (http.Handler, error) {
srv := APIServer{
APIConfig: *config,
Clock: clockwork.NewRealClock(),
@@ -246,15 +248,17 @@ func NewAPIServer(config *APIConfig) http.Handler {
srv.GET("/:version/events", srv.withAuth(srv.searchEvents))
srv.GET("/:version/events/session", srv.withAuth(srv.searchSessionEvents))
if plugin := GetPlugin(); plugin != nil {
plugin.AddHandlers(&srv)
if config.PluginRegistry != nil {
if err := config.PluginRegistry.RegisterAuthWebHandlers(&srv); err != nil {
return nil, trace.Wrap(err)
}
}
return httplib.RewritePaths(&srv.Router,
httplib.Rewrite("/v1/nodes", "/v1/namespaces/default/nodes"),
httplib.Rewrite("/v1/sessions", "/v1/namespaces/default/sessions"),
httplib.Rewrite("/v1/sessions/([^/]+)/(.*)", "/v1/namespaces/default/sessions/$1/$2"),
)
), nil
}
// HandlerWithAuthFunc is http handler with passed auth context
+10 -1
View File
@@ -1,5 +1,5 @@
/*
Copyright 2018-2020 Gravitational, Inc.
Copyright 2018-2021 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -57,6 +57,15 @@ type GRPCServer struct {
server *grpc.Server
}
// GetServer returns an instance of grpc server
func (g *GRPCServer) GetServer() (*grpc.Server, error) {
if g.server == nil {
return nil, trace.BadParameter("grpc server has not been initialized")
}
return g.server, nil
}
// EmitAuditEvent emits audit event
func (g *GRPCServer) EmitAuditEvent(ctx context.Context, req *events.OneOf) (*empty.Empty, error) {
auth, err := g.authenticate(ctx)
+14 -2
View File
@@ -1,5 +1,5 @@
/*
Copyright 2017-2020 Gravitational, Inc.
Copyright 2017-2021 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -126,7 +126,13 @@ func NewTLSServer(cfg TLSServerConfig) (*TLSServer, error) {
AcceptedUsage: cfg.AcceptedUsage,
Limiter: limiter,
}
authMiddleware.Wrap(NewAPIServer(&cfg.APIConfig))
apiServer, err := NewAPIServer(&cfg.APIConfig)
if err != nil {
return nil, trace.Wrap(err)
}
authMiddleware.Wrap(apiServer)
// Wrap sets the next middleware in chain to the authMiddleware
limiter.WrapHandle(authMiddleware)
// force client auth if given
@@ -163,6 +169,12 @@ func NewTLSServer(cfg TLSServerConfig) (*TLSServer, error) {
return nil, trace.Wrap(err)
}
if cfg.PluginRegistry != nil {
if err := cfg.PluginRegistry.RegisterAuthServices(server.grpcServer); err != nil {
return nil, trace.Wrap(err)
}
}
return server, nil
}
-44
View File
@@ -1,44 +0,0 @@
/*
Copyright 2017 Gravitational, Inc.
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 auth
import (
"sync"
)
var pluginMutex = &sync.Mutex{}
var plugin Plugin
// GetPlugin returns auth API server plugin that allows injecting handlers
func GetPlugin() Plugin {
pluginMutex.Lock()
defer pluginMutex.Unlock()
return plugin
}
// SetPlugin sets plugin for the auth API server
func SetPlugin(p Plugin) {
pluginMutex.Lock()
defer pluginMutex.Unlock()
plugin = p
}
// Plugin is auth API server extension setter
type Plugin interface {
// AddHandlers adds handlers to the auth API server
AddHandlers(srv *APIServer)
}
+2 -1
View File
@@ -63,7 +63,8 @@ func SetIndexHTMLHeaders(h http.Header) {
// Set content policy flags
var cspValue = strings.Join([]string{
"script-src 'self'",
// enterprise version uses stripe.com to update billing information
"script-src 'self' https://js.stripe.com",
// 'unsafe-inline' needed for reactjs inline styles
"style-src 'self' 'unsafe-inline'",
"object-src 'none'",
+108
View File
@@ -0,0 +1,108 @@
/*
Copyright 2015-2021 Gravitational, Inc.
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 plugin
import "github.com/gravitational/trace"
// Plugin describes interfaces of the teleport core plugin
type Plugin interface {
// GetName returns plugin name
GetName() string
// RegisterProxyWebHandlers registers new methods with the ProxyWebHandler
RegisterProxyWebHandlers(handler interface{}) error
// RegisterAuthWebHandlers registers new methods with the Auth Web Handler
RegisterAuthWebHandlers(service interface{}) error
// RegisterAuthServices registers new services on the AuthServer
RegisterAuthServices(server interface{}) error
}
// Registry is the plugin registry
type Registry interface {
// Add adds plugin to the registry
Add(plugin Plugin) error
// RegisterProxyWebHandlers registers Teleport Proxy web handlers
RegisterProxyWebHandlers(hander interface{}) error
// RegisterAuthWebHandlers registers Teleport Auth web handlers
RegisterAuthWebHandlers(handler interface{}) error
// RegisterAuthServices registerse Teleport AuthServer services
RegisterAuthServices(server interface{}) error
}
// NewRegistry creates an instance of the Registry
func NewRegistry() Registry {
return &registry{
plugins: make(map[string]Plugin),
}
}
type registry struct {
plugins map[string]Plugin
}
// Add adds plugin to the plugin registry
func (r *registry) Add(p Plugin) error {
if p == nil {
return trace.BadParameter("missing plugin")
}
name := p.GetName()
if name == "" {
return trace.BadParameter("missing plugin name")
}
_, exists := r.plugins[name]
if exists {
return trace.AlreadyExists("plugin %v already exists", name)
}
r.plugins[name] = p
return nil
}
// RegisterProxyWebHandlers registers Teleport Proxy web handlers
func (r *registry) RegisterProxyWebHandlers(hander interface{}) error {
for _, p := range r.plugins {
if err := p.RegisterProxyWebHandlers(hander); err != nil {
return trace.Wrap(err, "plugin %v failed to register", p.GetName())
}
}
return nil
}
// RegisterAuthWebHandlers registers Teleport Auth web handlers
func (r *registry) RegisterAuthWebHandlers(handler interface{}) error {
for _, p := range r.plugins {
if err := p.RegisterAuthWebHandlers(handler); err != nil {
return trace.Wrap(err, "plugin %v failed to register", p.GetName())
}
}
return nil
}
// RegisterAuthServices registerse Teleport AuthServer services
func (r *registry) RegisterAuthServices(server interface{}) error {
for _, p := range r.plugins {
if err := p.RegisterAuthServices(server); err != nil {
return trace.Wrap(err, "plugin %v failed to register", p.GetName())
}
}
return nil
}
+5 -1
View File
@@ -1,5 +1,5 @@
/*
Copyright 2015-2020 Gravitational, Inc.
Copyright 2015-2021 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -38,6 +38,7 @@ import (
"github.com/gravitational/teleport/lib/events"
"github.com/gravitational/teleport/lib/limiter"
"github.com/gravitational/teleport/lib/pam"
"github.com/gravitational/teleport/lib/plugin"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/sshca"
"github.com/gravitational/teleport/lib/tlsca"
@@ -200,6 +201,9 @@ type Config struct {
// Log optionally specifies the logger
Log utils.Logger
// PluginRegistry allows adding enterprise logic to Teleport services
PluginRegistry plugin.Registry
}
// ApplyToken assigns a given token to all internal services but only if token
+1 -1
View File
@@ -187,7 +187,7 @@ func (process *TeleportProcess) initDatabaseService() (retErr error) {
}()
// Execute this when the process running database proxy service exits.
process.onExit("db.stop", func(payload interface{}) {
process.OnExit("db.stop", func(payload interface{}) {
log.Info("Shutting down.")
if dbService != nil {
warnOnErr(dbService.Close(), process.log)
+1 -1
View File
@@ -265,7 +265,7 @@ func (process *TeleportProcess) initKubernetesService(log *logrus.Entry, conn *C
})
// Cleanup, when process is exiting.
process.onExit("kube.shutdown", func(payload interface{}) {
process.OnExit("kube.shutdown", func(payload interface{}) {
if asyncEmitter != nil {
warnOnErr(asyncEmitter.Close(), log)
}
+38 -27
View File
@@ -1,5 +1,5 @@
/*
Copyright 2015-2020 Gravitational, Inc.
Copyright 2015-2021 Gravitational, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -69,6 +69,7 @@ import (
"github.com/gravitational/teleport/lib/limiter"
"github.com/gravitational/teleport/lib/modules"
"github.com/gravitational/teleport/lib/multiplexer"
"github.com/gravitational/teleport/lib/plugin"
"github.com/gravitational/teleport/lib/reversetunnel"
"github.com/gravitational/teleport/lib/services"
"github.com/gravitational/teleport/lib/session"
@@ -241,6 +242,9 @@ type TeleportProcess struct {
Supervisor
Config *Config
// PluginsRegistry handles plugin registrations with Teleport services
PluginRegistry plugin.Registry
// localAuth has local auth server listed in case if this process
// has started with auth server role enabled
localAuth *auth.Server
@@ -627,7 +631,12 @@ func NewTeleport(cfg *Config) (*TeleportProcess, error) {
cfg.Clock = clockwork.NewRealClock()
}
if cfg.PluginRegistry == nil {
cfg.PluginRegistry = plugin.NewRegistry()
}
process := &TeleportProcess{
PluginRegistry: cfg.PluginRegistry,
Clock: cfg.Clock,
Supervisor: supervisor,
Config: cfg,
@@ -1180,6 +1189,7 @@ func (process *TeleportProcess) initAuthService() error {
SessionService: sessionService,
Authorizer: authorizer,
AuditLog: process.auditLog,
PluginRegistry: process.PluginRegistry,
Emitter: checkingEmitter,
MetadataGetter: uploadHandler,
}
@@ -1271,7 +1281,7 @@ func (process *TeleportProcess) initAuthService() error {
}
// External integrations rely on this event:
process.BroadcastEvent(Event{Name: AuthIdentityEvent, Payload: connector})
process.onExit("auth.broadcast", func(payload interface{}) {
process.OnExit("auth.broadcast", func(payload interface{}) {
connector.Close()
})
return nil
@@ -1354,7 +1364,7 @@ func (process *TeleportProcess) initAuthService() error {
}
process.RegisterFunc("auth.heartbeat", heartbeat.Run)
// execute this when process is asked to exit:
process.onExit("auth.shutdown", func(payload interface{}) {
process.OnExit("auth.shutdown", func(payload interface{}) {
// The listeners have to be closed here, because if shutdown
// was called before the start of the http server,
// the http server would have not started tracking the listeners
@@ -1390,10 +1400,10 @@ func payloadContext(payload interface{}, log logrus.FieldLogger) context.Context
return context.TODO()
}
// onExit allows individual services to register a callback function which will be
// OnExit allows individual services to register a callback function which will be
// called when Teleport Process is asked to exit. Usually services terminate themselves
// when the callback is called
func (process *TeleportProcess) onExit(serviceName string, callback func(interface{})) {
func (process *TeleportProcess) OnExit(serviceName string, callback func(interface{})) {
process.RegisterFunc(serviceName, func() error {
eventC := make(chan Event)
process.WaitForEvent(context.TODO(), TeleportExitEvent, eventC)
@@ -1810,7 +1820,7 @@ func (process *TeleportProcess) initSSH() error {
})
// Execute this when process is asked to exit.
process.onExit("ssh.shutdown", func(payload interface{}) {
process.OnExit("ssh.shutdown", func(payload interface{}) {
if payload == nil {
log.Infof("Shutting down immediately.")
if s != nil {
@@ -1851,7 +1861,7 @@ func (process *TeleportProcess) registerWithAuthServer(role teleport.Role, event
if err != nil {
return trace.Wrap(err)
}
process.onExit(fmt.Sprintf("auth.client.%v", serviceName), func(interface{}) {
process.OnExit(fmt.Sprintf("auth.client.%v", serviceName), func(interface{}) {
process.log.Debugf("Closed client for %v.", role)
err := connector.Client.Close()
if err != nil {
@@ -1923,7 +1933,7 @@ func (process *TeleportProcess) initUploaderService(accessPoint auth.AccessPoint
return nil
})
process.onExit("uploader.shutdown", func(payload interface{}) {
process.OnExit("uploader.shutdown", func(payload interface{}) {
log.Infof("Shutting down.")
warnOnErr(uploader.Stop(), log)
log.Infof("Exited.")
@@ -1949,7 +1959,7 @@ func (process *TeleportProcess) initUploaderService(accessPoint auth.AccessPoint
return nil
})
process.onExit("fileuploader.shutdown", func(payload interface{}) {
process.OnExit("fileuploader.shutdown", func(payload interface{}) {
log.Infof("File uploader is shutting down.")
warnOnErr(fileUploader.Close(), log)
log.Infof("File uploader has shut down.")
@@ -2048,7 +2058,7 @@ func (process *TeleportProcess) initDiagnosticService() error {
return nil
})
process.onExit("diagnostic.shutdown", func(payload interface{}) {
process.OnExit("diagnostic.shutdown", func(payload interface{}) {
if payload == nil {
log.Infof("Shutting down immediately.")
warnOnErr(server.Close(), log)
@@ -2501,20 +2511,21 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error {
}
webHandler, err = web.NewHandler(
web.Config{
Proxy: tsrv,
AuthServers: cfg.AuthServers[0],
DomainName: cfg.Hostname,
ProxyClient: conn.Client,
ProxySSHAddr: proxySSHAddr,
ProxyWebAddr: cfg.Proxy.WebAddr,
ProxySettings: proxySettings,
CipherSuites: cfg.CipherSuites,
FIPS: cfg.FIPS,
AccessPoint: accessPoint,
Emitter: streamEmitter,
HostUUID: process.Config.HostUUID,
Context: process.ExitContext(),
StaticFS: fs,
Proxy: tsrv,
AuthServers: cfg.AuthServers[0],
DomainName: cfg.Hostname,
ProxyClient: conn.Client,
ProxySSHAddr: proxySSHAddr,
ProxyWebAddr: cfg.Proxy.WebAddr,
ProxySettings: proxySettings,
CipherSuites: cfg.CipherSuites,
FIPS: cfg.FIPS,
AccessPoint: accessPoint,
Emitter: streamEmitter,
PluginRegistry: process.PluginRegistry,
HostUUID: process.Config.HostUUID,
Context: process.ExitContext(),
StaticFS: fs,
})
if err != nil {
return trace.Wrap(err)
@@ -2756,7 +2767,7 @@ func (process *TeleportProcess) initProxyEndpoint(conn *Connector) error {
}
// execute this when process is asked to exit:
process.onExit("proxy.shutdown", func(payload interface{}) {
process.OnExit("proxy.shutdown", func(payload interface{}) {
rcWatcher.Close()
defer listeners.Close()
// Need to shut down this listener first, because
@@ -3045,7 +3056,7 @@ func (process *TeleportProcess) initApps() {
})
// Execute this when process is asked to exit.
process.onExit("apps.stop", func(payload interface{}) {
process.OnExit("apps.stop", func(payload interface{}) {
log.Infof("Shutting down.")
if appServer != nil {
warnOnErr(appServer.Close(), log)
@@ -3260,7 +3271,7 @@ func (process *TeleportProcess) initDebugApp() {
process.BroadcastEvent(Event{Name: DebugAppReady, Payload: server})
return nil
})
process.onExit("debug.app.shutdown", func(payload interface{}) {
process.OnExit("debug.app.shutdown", func(payload interface{}) {
if server != nil {
server.Close()
}
+1
View File
@@ -484,6 +484,7 @@ const (
KindClusterAuthPreference = types.KindClusterAuthPreference
MetaNameClusterAuthPreference = types.MetaNameClusterAuthPreference
KindClusterConfig = types.KindClusterConfig
KindBilling = types.KindBilling
KindSemaphore = types.KindSemaphore
MetaNameClusterConfig = types.MetaNameClusterConfig
KindClusterName = types.KindClusterName
+8 -3
View File
@@ -46,6 +46,7 @@ import (
"github.com/gravitational/teleport/lib/httplib"
"github.com/gravitational/teleport/lib/httplib/csrf"
"github.com/gravitational/teleport/lib/jwt"
"github.com/gravitational/teleport/lib/plugin"
"github.com/gravitational/teleport/lib/reversetunnel"
"github.com/gravitational/teleport/lib/secret"
"github.com/gravitational/teleport/lib/services"
@@ -104,6 +105,8 @@ func SetClock(clock clockwork.Clock) HandlerOption {
// Config represents web handler configuration parameters
type Config struct {
// PluginRegistry handles plugin registration
PluginRegistry plugin.Registry
// Proxy is a reverse tunnel proxy that handles connections
// to local cluster or remote clusters using unified interface
Proxy reversetunnel.Tunnel
@@ -423,9 +426,11 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*RewritingHandler, error) {
})
h.NotFound = routingHandler
plugin := GetPlugin()
if plugin != nil {
plugin.AddHandlers(h)
if cfg.PluginRegistry != nil {
if err := cfg.PluginRegistry.RegisterProxyWebHandlers(h); err != nil {
return nil, trace.Wrap(err)
}
}
// Create application specific handler. This handler handles sessions and
-45
View File
@@ -1,45 +0,0 @@
/*
Copyright 2017 Gravitational, Inc.
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 web
import (
"sync"
)
var pluginMutex = &sync.Mutex{}
var plugin Plugin
// GetPlugin returns web API plugin that
func GetPlugin() Plugin {
pluginMutex.Lock()
defer pluginMutex.Unlock()
return plugin
}
// SetPlugin sets plugin for the web handler
func SetPlugin(p Plugin) {
pluginMutex.Lock()
defer pluginMutex.Unlock()
plugin = p
}
// Plugin is API Server extension setter
// if set, it will add handler methods during web handler initialization
type Plugin interface {
// AddHandlers adds handlers to the web API handler
AddHandlers(h *Handler)
}
+9 -1
View File
@@ -47,6 +47,7 @@ import (
"github.com/jonboulle/clockwork"
"github.com/sirupsen/logrus"
"google.golang.org/grpc"
)
// SessionContext is a context associated with a user's
@@ -56,7 +57,7 @@ import (
type SessionContext struct {
log logrus.FieldLogger
user string
clt auth.ClientI
clt *auth.Client
parent *sessionCache
// resources is persistent resource store this context is bound to.
// The store maintains a list of resources between session renewals
@@ -121,6 +122,11 @@ func (c *SessionContext) GetClient() (auth.ClientI, error) {
return c.clt, nil
}
// GetClientConnection returns a connection to Auth Service
func (c *SessionContext) GetClientConnection() *grpc.ClientConn {
return c.clt.GetConnection()
}
// GetUserClient will return an auth.ClientI with the role of the user at
// the requested site. If the site is local a client with the users local role
// is returned. If the site is remote a client with the users remote role is
@@ -737,6 +743,7 @@ func (s *sessionCache) newSessionContextFromSession(session services.WebSession)
if err != nil {
return nil, trace.Wrap(err)
}
ctx := &SessionContext{
clt: userClient,
remoteClt: make(map[string]auth.ClientI),
@@ -757,6 +764,7 @@ func (s *sessionCache) newSessionContextFromSession(session services.WebSession)
// close our extra context and return
ctx.Close()
}
return ctx, nil
}
+7 -3
View File
@@ -56,12 +56,14 @@ type userACL struct {
Tokens access `json:"tokens"`
// Nodes defines access to nodes.
Nodes access `json:"nodes"`
// AppServers defines access to application servers.
// AppServers defines access to application servers
AppServers access `json:"appServers"`
// SSH defines access to servers
SSHLogins []string `json:"sshLogins"`
// AccessRequests defines access to access requests.
// AccessRequests defines access to access requests
AccessRequests access `json:"accessRequests"`
// Billing defines access to billing information
Billing access `json:"billing"`
}
type authType string
@@ -71,7 +73,7 @@ const (
authSSO authType = "sso"
)
// UserContext describes a users settings to various resources.
// UserContext describes user settings and access to various resources.
type UserContext struct {
// AuthType is auth method of this user.
AuthType authType `json:"authType"`
@@ -168,6 +170,7 @@ func NewUserContext(user services.User, userRoles services.RoleSet) (*UserContex
nodeAccess := newAccess(userRoles, ctx, services.KindNode)
appServerAccess := newAccess(userRoles, ctx, services.KindAppServer)
requestAccess := newAccess(userRoles, ctx, services.KindAccessRequest)
billingAccess := newAccess(userRoles, ctx, services.KindBilling)
logins := getLogins(userRoles)
accessStrategy := getAccessStrategy(userRoles)
@@ -184,6 +187,7 @@ func NewUserContext(user services.User, userRoles services.RoleSet) (*UserContex
Users: userAccess,
Tokens: tokenAccess,
Nodes: nodeAccess,
Billing: billingAccess,
}
// local user