iam-authz-server源码学习

文章详细介绍了iam-authz-server的启动流程,包括初始化Redis、启动授权服务和日志服务。iam-authz-server主要通过缓存策略来提高性能,当策略或密钥发生变化时,通过redis订阅机制进行更新。授权流程中,使用ladon进行资源访问控制,日志通过Analytics服务异步写入redis。整个架构保证了数据流组件的高性能和数据一致性。
摘要由CSDN通过智能技术生成

iam-authz-server源码学习

之前大概看了iam-apiserver的启动流程和API请求处理流程,今天记录下iam-authz-server的学习。
iam-authz-server目前的功能只有一个,就是完成资源授权,但因为这个应用承载了数据流的请求,需要确保API接口具有较高的性能,因此作者使用了大量的缓存技术,值得学习。

一.iam-authz-server启动流程

iam-authz-server的服务框架设计与iam-apiserver的保持一致,也是有3种配置:Options配置,组件配置和HTTP服务配置,启动流程也是类似。
入口为:cmd/iam-authz-server/authzserver.go

func main() {
	rand.Seed(time.Now().UTC().UnixNano())
	if len(os.Getenv("GOMAXPROCS")) == 0 {
		runtime.GOMAXPROCS(runtime.NumCPU())
	}

	authzserver.NewApp("iam-authz-server").Run()
}

这里与iam-apiserver一样,共用了APP包,因此启动流程也是类似的。
不同的地方在Options配置:

type Options struct {
	RPCServer               string                                 `json:"rpcserver"      mapstructure:"rpcserver"`
	ClientCA                string                                 `json:"client-ca-file" mapstructure:"client-ca-file"`
	GenericServerRunOptions *genericoptions.ServerRunOptions       `json:"server"         mapstructure:"server"`
	InsecureServing         *genericoptions.InsecureServingOptions `json:"insecure"       mapstructure:"insecure"`
	SecureServing           *genericoptions.SecureServingOptions   `json:"secure"         mapstructure:"secure"`
	RedisOptions            *genericoptions.RedisOptions           `json:"redis"          mapstructure:"redis"`
	FeatureOptions          *genericoptions.FeatureOptions         `json:"feature"        mapstructure:"feature"`
	Log                     *log.Options                           `json:"log"            mapstructure:"log"`
	AnalyticsOptions        *analytics.AnalyticsOptions            `json:"analytics"      mapstructure:"analytics"`
}

// NewOptions creates a new Options object with default parameters.
func NewOptions() *Options {
	o := Options{
		RPCServer:               "127.0.0.1:8081",
		ClientCA:                "",
		GenericServerRunOptions: genericoptions.NewServerRunOptions(),
		InsecureServing:         genericoptions.NewInsecureServingOptions(),
		SecureServing:           genericoptions.NewSecureServingOptions(),
		RedisOptions:            genericoptions.NewRedisOptions(),
		FeatureOptions:          genericoptions.NewFeatureOptions(),
		Log:                     log.NewOptions(),
		AnalyticsOptions:        analytics.NewAnalyticsOptions(),
	}

	return &o
}

iam-authz-server多了一个AnalyticsOptions,用来配置Analytics服务,这个服务会将授权日志异步写入redis中。
组件配置部分也是保持与Options一样的:

type Config struct {
	*options.Options
}

// CreateConfigFromOptions creates a running configuration instance based
// on a given IAM pump command line or configuration file option.
func CreateConfigFromOptions(opts *options.Options) (*Config, error) {
	return &Config{opts}, nil
}

然后是执行Run()方法:

func Run(cfg *config.Config) error {
	server, err := createAuthzServer(cfg)
	if err != nil {
		return err
	}

	return server.PrepareRun().Run()
}

流程上与iam-apiserver保持一致,也是通过构建http服务,然后调用PrepareRun(),Run()
只是iam-authz-server不需要Grpc,所以没有构建这个服务。还有启动流程一些配置不一样,接下来看看PrePareRun()和Run()都做了什么

func (s *authzServer) PrepareRun() preparedAuthzServer {
	_ = s.initialize()

	initRouter(s.genericAPIServer.Engine)

	return preparedAuthzServer{s}
}

首先调用了initialize()进行了一些初始化:

func (s *authzServer) initialize() error {
	ctx, cancel := context.WithCancel(context.Background())
	s.redisCancelFunc = cancel

	// keep redis connected
	go storage.ConnectToRedis(ctx, s.buildStorageConfig())

	// cron to reload all secrets and policies from iam-apiserver
	cacheIns, err := cache.GetCacheInsOr(apiserver.GetAPIServerFactoryOrDie(s.rpcServer, s.clientCA))
	if err != nil {
		return errors.Wrap(err, "get cache instance failed")
	}

	load.NewLoader(ctx, cacheIns).Start()

	// start analytics service
	if s.analyticsOptions.Enable {
		analyticsStore := storage.RedisCluster{KeyPrefix: RedisKeyPrefix}
		analyticsIns := analytics.NewAnalytics(s.analyticsOptions, &analyticsStore)
		analyticsIns.Start()
	}

	return nil
}

初始化了Redis,并且启动一个goroutine去连接redis,失败会重连;启动一个Loader是一个缓存服务,用于将授权策略和密钥缓存,并同步数据库;最后通过配置文件的analyticsOptions.Enable判断是否开启将授权日志写入redis中。

接着就是初始化路由了:

func initRouter(g *gin.Engine) {
	installMiddleware(g)
	installController(g)
}

func installMiddleware(g *gin.Engine) {
}

func installController(g *gin.Engine) *gin.Engine {
	auth := newCacheAuth()
	g.NoRoute(auth.AuthFunc(), func(c *gin.Context) {
		core.WriteResponse(c, errors.WithCode(code.ErrPageNotFound, "page not found."), nil)
	})

	cacheIns, _ := cache.GetCacheInsOr(nil)
	if cacheIns == nil {
		log.Panicf("get nil cache instance")
	}

	apiv1 := g.Group("/v1", auth.AuthFunc())
	{
		authzController := authorize.NewAuthzController(cacheIns)

		// Router for authorization
		apiv1.POST("/authz", authzController.Authorize)
	}

	return g
}

这里只有一个API接口:/v1/authz,最后调用Run()启动Http服务:

func (s preparedAuthzServer) Run() error {
	stopCh := make(chan struct{})

	// start shutdown managers
	if err := s.gs.Start(); err != nil {
		log.Fatalf("start shutdown manager failed: %s", err.Error())
	}

	//nolint: errcheck
	go s.genericAPIServer.Run()

	// in order to ensure that the reported data is not lost,
	// please ensure the following graceful shutdown sequence
	s.gs.AddShutdownCallback(shutdown.ShutdownFunc(func(string) error {
		s.genericAPIServer.Close()
		if s.analyticsOptions.Enable {
			analytics.GetAnalytics().Stop()
		}
		s.redisCancelFunc()

		return nil
	}))

	// blocking here via channel to prevents the process exit.
	<-stopCh

	return nil
}

到这整个启动流程就走完了,细节部分先不关注。

二.API请求处理流程

iam-authz-server的请求处理流程也是分层处理的,也是清晰,规范的。
首先,在installController中创建了authzController,并指定了接口的Handler:

apiv1 := g.Group("/v1", auth.AuthFunc())
	{
		authzController := authorize.NewAuthzController(cacheIns)

		// Router for authorization
		apiv1.POST("/authz", authzController.Authorize)
	}

/v1/authz接口是用于对资源的访问授权的,对应的处理方法是Authorize

func (a *AuthzController) Authorize(c *gin.Context) {
	var r ladon.Request
	if err := c.ShouldBind(&r); err != nil {
		core.WriteResponse(c, errors.WithCode(code.ErrBind, err.Error()), nil)

		return
	}

	auth := authorization.NewAuthorizer(authorizer.NewAuthorization(a.store))
	if r.Context == nil {
		r.Context = ladon.Context{}
	}

	r.Context["username"] = c.GetString("username")
	rsp := auth.Authorize(&r)

	core.WriteResponse(c, nil, rsp)
}

这个函数是使用了github.com/ory/ladon包进行资源访问授权的,关于这个包的学习可以看看这个Ladon 折腾手记:结合 Gin 开发一个简易 ACL 接口
授权流程如下图:
在这里插入图片描述

首先,第一步,调用了c.ShouldBind(&r),将API请求参数解析到ladon.Request类型的结构体变量中
第二步是调用了authorization.NewAuthorizer函数,这个函数实际上就是ladon的权限控制,ladon的ladon.Ladon实现了ladon.Warden接口,包含了其中两个字段:Manager和AuditLogger
Manager是用来对授权策略的管理的,包含增删改查;AuditLogger是用来记录审计的,包含LogRejectedAccessRequestLogGrantedAccessRequest函数,记录被拒绝的授权请求和被允许的授权请求。

type Authorizer struct {
	warden ladon.Warden
}

// NewAuthorizer creates a local repository authorizer and returns it.
func NewAuthorizer(authorizationClient AuthorizationInterface) *Authorizer {
	return &Authorizer{
		warden: &ladon.Ladon{
			Manager:     NewPolicyManager(authorizationClient),
			AuditLogger: NewAuditLogger(authorizationClient),
		},
	}
}

ladon默认实现了Manger是保存在内存中的 memory.NewMemoryManager(),AuditLogger则是&ladon.AuditLoggerInfo{},都是接口的实现,我们可以自己实现自己的功能,比如作者这里就是自己实现了接口

func (a *Authorizer) Authorize(request *ladon.Request) *authzv1.Response {
	log.Debug("authorize request", log.Any("request", request))

	if err := a.warden.IsAllowed(request); err != nil {
		return &authzv1.Response{
			Denied: true,
			Reason: err.Error(),
		}
	}

	return &authzv1.Response{
		Allowed: true,
	}
}

这里看下IsAllowed()方法的实现:

func (l *Ladon) IsAllowed(r *Request) (err error) {
	policies, err := l.Manager.FindRequestCandidates(r)
	if err != nil {
		go l.metric().RequestProcessingError(*r, nil, err)
		return err
	}

	// Although the manager is responsible of matching the policies, it might decide to just scan for
	// subjects, it might return all policies, or it might have a different pattern matching than Golang.
	// Thus, we need to make sure that we actually matched the right policies.
	return l.DoPoliciesAllow(r, policies)

他调用了Manager.FindRequestCandidates`方法查询所有的授权策略

func (m *PolicyManager) FindRequestCandidates(r *ladon.Request) (ladon.Policies, error) {
	username := ""

	if user, ok := r.Context["username"].(string); ok {
		username = user
	}

	policies, err := m.client.List(username)
	if err != nil {
		return nil, errors.Wrap(err, "list policies failed")
	}

	ret := make([]ladon.Policy, 0, len(policies))
	for _, policy := range policies {
		ret = append(ret, policy)
	}

	return ret, nil
}

因为iam中是查询请求用户的授权策略列表,所以在请求开始时将用户名放入了ladon的Context中:r.Context["username"] = c.GetString("username")
FindRequestCandidates()方法中将用户名拿出来,再调用m.client.List(username)列出该用户的所有授权策略,这里的List方法是保存授权策略的缓存的一个方法,后面再看这一块的。
最后,IsAllowed函数调用了DoPoliciesAllow()方法,对用户的所有授权策略与请求进行匹配。

三. 接口设计

iam的架构设计是这样的:
在这里插入图片描述

其中红色圈出部分为iam-authz-server的架构设计,这个服务目前只提供了一个api,对请求访问授权,其中密钥和授权策略都是在iam-apiserver中CRUD的,iam-authz-server通过grpc从iam-apiserver中获取,
然后缓存到内存(ristretto)中,同时通过redis的发布订阅,监听iam-apiserver的密钥和授权策略的变化,并且授权部分使用了第三方包ladon。
接下来就来学习下iam-authz-server中这一部分的接口设计,我们从授权这一块入手:

  1. 首先在创建Controller时,就传入了一个参数cacheIns
func installController(g *gin.Engine) *gin.Engine {
	auth := newCacheAuth()
	g.NoRoute(auth.AuthFunc(), func(c *gin.Context) {
		core.WriteResponse(c, errors.WithCode(code.ErrPageNotFound, "page not found."), nil)
	})

	cacheIns, _ := cache.GetCacheInsOr(nil)
	if cacheIns == nil {
		log.Panicf("get nil cache instance")
	}

	apiv1 := g.Group("/v1", auth.AuthFunc())
	{
		authzController := authorize.NewAuthzController(cacheIns)

		// Router for authorization
		apiv1.POST("/authz", authzController.Authorize)
	}

	return g
}

这个cacheIns是缓存的实例,因为这里只用到了缓存:

func GetCacheInsOr(cli store.Factory) (*Cache, error) {
	var err error
	if cli != nil {
		var (
			secretCache *ristretto.Cache
			policyCache *ristretto.Cache
		)

		onceCache.Do(func() {
			c := &ristretto.Config{
				NumCounters: 1e7,     // number of keys to track frequency of (10M).
				MaxCost:     1 << 30, // maximum cost of cache (1GB).
				BufferItems: 64,      // number of keys per Get buffer.
				Cost:        nil,
			}

			secretCache, err = ristretto.NewCache(c)
			if err != nil {
				return
			}
			policyCache, err = ristretto.NewCache(c)
			if err != nil {
				return
			}

			cacheIns = &Cache{
				cli:      cli,
				lock:     new(sync.RWMutex),
				secrets:  secretCache,
				policies: policyCache,
			}
		})
	}

	return cacheIns, err
}

可以看到这里使用了内存库是ristretto,这个方法是获取一个缓存的实例,在最开始的启动流程中已经生成了一个缓存的实例的了,并且也已经开启了连接redis的goroutine:

func (s *authzServer) initialize() error {
	ctx, cancel := context.WithCancel(context.Background())
	s.redisCancelFunc = cancel

	// keep redis connected
	go storage.ConnectToRedis(ctx, s.buildStorageConfig())

	// cron to reload all secrets and policies from iam-apiserver
	cacheIns, err := cache.GetCacheInsOr(apiserver.GetAPIServerFactoryOrDie(s.rpcServer, s.clientCA))
	if err != nil {
		return errors.Wrap(err, "get cache instance failed")
	}

	load.NewLoader(ctx, cacheIns).Start()

	// start analytics service
	if s.analyticsOptions.Enable {
		analyticsStore := storage.RedisCluster{KeyPrefix: RedisKeyPrefix}
		analyticsIns := analytics.NewAnalytics(s.analyticsOptions, &analyticsStore)
		analyticsIns.Start()
	}

	return nil
}

再看看apiserver.GetAPIServerFactoryOrDie(s.rpcServer, s.clientCA):

type datastore struct {
	cli pb.CacheClient
}

func (ds *datastore) Secrets() store.SecretStore {
	return newSecrets(ds)
}

func (ds *datastore) Policies() store.PolicyStore {
	return newPolicies(ds)
}

func GetAPIServerFactoryOrDie(address string, clientCA string) store.Factory {
	once.Do(func() {
		var (
			err   error
			conn  *grpc.ClientConn
			creds credentials.TransportCredentials
		)

		creds, err = credentials.NewClientTLSFromFile(clientCA, "")
		if err != nil {
			log.Panicf("credentials.NewClientTLSFromFile err: %v", err)
		}

		conn, err = grpc.Dial(address, grpc.WithBlock(), grpc.WithTransportCredentials(creds))
		if err != nil {
			log.Panicf("Connect to grpc server failed, error: %s", err.Error())
		}

		apiServerFactory = &datastore{pb.NewCacheClient(conn)}
		log.Infof("Connected to grpc server, address: %s", address)
	})

	if apiServerFactory == nil {
		log.Panicf("failed to get apiserver store fatory")
	}

	return apiServerFactory
}

这里是通过grpc连接iam-apiserver,返回一个store.Factory接口的实例apiServerFactory,使用了工厂方法模式,创建密钥secret和策略policy的实例,再通过grpc从iam-apiserver中获取密钥和策略

  1. 接着将这个apiServerFactory作为参数去实例化cache,然后通过这个cache实例作为参数,从创建Controller开始传递进去,对于授权部分,作者定义了一个authorization包放置对应的源文件:
    在这里插入图片描述

接着看下这个Controller的定义:

type AuthzController struct {
	store authorizer.PolicyGetter
}

// NewAuthzController creates a authorize handler.
func NewAuthzController(store authorizer.PolicyGetter) *AuthzController {
	return &AuthzController{
		store: store,
	}
}

AuthzController结构有一个store字段,它又是一个接口类型authorizer.PolicyGetter

type PolicyGetter interface {
	GetPolicy(key string) ([]*ladon.DefaultPolicy, error)
}

这个接口只有一个方法GetPolicy,只要实现了这个方法的结构,都认为实现了这个接口,在cache这个实例中实现了这个获取策略的方法,因而实现了这个接口。
最后的路由是:apiv1.POST("/authz", authzController.Authorize),处理函数是authzController的Authorize方法,这个方法上面已经讨论过,就是授权的实现。

  1. 最后看授权时是怎么拿到缓存的数据的,在Authorize方法中创建一个Authorizer:auth := authorization.NewAuthorizer(authorizer.NewAuthorization(a.store))
    这里还是封了一层,NewAuthorizer接收的是一个AuthorizationInterface,这是一个接口:
// AuthorizationInterface defiens the CURD method for lady policy.
type AuthorizationInterface interface {
	Create(*ladon.DefaultPolicy) error
	Update(*ladon.DefaultPolicy) error
	Delete(id string) error
	DeleteCollection(idList []string) error
	Get(id string) (*ladon.DefaultPolicy, error)
	List(username string) ([]*ladon.DefaultPolicy, error)

	// The following two functions tracks denied and granted authorizations.
	LogRejectedAccessRequest(request *ladon.Request, pool ladon.Policies, deciders ladon.Policies)
	LogGrantedAccessRequest(request *ladon.Request, pool ladon.Policies, deciders ladon.Policies)
}

而NewAuthorization创建的实例是实现了这个接口的,同时接收的参数是cache实例,因此在这个Authorization实例中能够对授权策略进行CRUD。

  1. 最后看看ladon这一块,这里实现了ladon的Manager和Logger接口,在Manager的FindRequestCandidates方法里查找用户的授权策略,最后会在ladon的IsAllowed方法调用,最后完成授权

四.缓存设计

因为iam-authz-server主要用阿里做资源访问授权,属于数据流的组件,对接口访问性能有比较高的要求,所以该组件采用了缓存的机制。
在这里插入图片描述

上面说过,当密钥或授权策略有更新或变化时,iam-authz-server是通过redis的订阅来接收的,这一部分来看看代码
因为用到了redis,第一步肯定需要先建立redis连接的,这部分代码在初始化函数initialize中:
go storage.ConnectToRedis(ctx, s.buildStorageConfig()),这个代码会维护一个redis连接,如果redis断开会尝试重连

1. 密钥和策略缓存机制

iam-authz-server通过load包来完成密钥和策略的缓存。
在这里插入图片描述

在iam-authz-server启动时会创建并启动一个Load服务,也是在初始化函数initialize中:load.NewLoader(ctx, cacheIns).Start()
我们来看看Load服务的创建:

// Loader defines function to reload storage.
type Loader interface {
	Reload() error
}

// Load is used to reload given storage.
type Load struct {
	ctx    context.Context
	lock   *sync.RWMutex
	loader Loader
}

// NewLoader return a loader with a loader implement.
func NewLoader(ctx context.Context, loader Loader) *Load {
	return &Load{
		ctx:    ctx,
		lock:   new(sync.RWMutex),
		loader: loader,
	}
}

创建Load服务时,传入了cacheIns参数,这个cacheIns就是Cache实例,它还实现了Load接口的方法Reload
然后启动Load服务,调用了Start方法:

// Start start a loop service.
func (l *Load) Start() {
	go startPubSubLoop()
	go l.reloadQueueLoop()
	// 1s is the minimum amount of time between hot reloads. The
	// interval counts from the start of one reload to the next.
	go l.reloadLoop()
	l.DoReload()
}

Start方法启动了3个goroutine,在调用l.DoReload()进行了一次密钥和策略的同步:

// DoReload reload secrets and policies.
func (l *Load) DoReload() {
	l.lock.Lock()
	defer l.lock.Unlock()

	if err := l.loader.Reload(); err != nil {
		log.Errorf("faild to refresh target storage: %s", err.Error())
	}

	log.Debug("refresh target storage succ")
}

这里DoReload方法调用了loader的Reload方法,这个loader是一个接口实例,是从上面的cacheIns传递进来的,这样子处理对于Loader服务来说不用关系具体怎么实现的Reload的,对于外层实现Reload,现在这里是从iam-apiserver中通过grpc同步密钥和策略信息到缓存中,利用了接口,对于外层实现Reload可以有多种实现方式,改变一种方式也不会影响到Load服务。

接下来再看看Start方法里的三个goroutine具体做了啥

  1. startPubSubLoop
func startPubSubLoop() {
	cacheStore := storage.RedisCluster{}
	cacheStore.Connect()
	// On message, synchronize
	for {
		err := cacheStore.StartPubSubHandler(RedisPubSubChannel, func(v interface{}) {
			handleRedisEvent(v, nil, nil)
		})
		if err != nil {
			if !errors.Is(err, storage.ErrRedisIsDown) {
				log.Errorf("Connection to Redis failed, reconnect in 10s: %s", err.Error())
			}

			time.Sleep(10 * time.Second)
			log.Warnf("Reconnecting: %s", err.Error())
		}
	}
}

这个方法通过StartPubSubHandler函数订阅Redis的channel:iam.cluster.notifications,注册回调函数handleRedisEvent处理redis订阅的channel消息:

func handleRedisEvent(v interface{}, handled func(NotificationCommand), reloaded func()) {
	message, ok := v.(*redis.Message)
	if !ok {
		return
	}

	notif := Notification{}
	if err := json.Unmarshal([]byte(message.Payload), &notif); err != nil {
		log.Errorf("Unmarshalling message body failed, malformed: ", err)

		return
	}
	log.Infow("receive redis message", "command", notif.Command, "payload", message.Payload)

	switch notif.Command {
	case NoticePolicyChanged, NoticeSecretChanged:
		log.Info("Reloading secrets and policies")
		reloadQueue <- reloaded
	default:
		log.Warnf("Unknown notification command: %q", notif.Command)

		return
	}

	if handled != nil {
		// went through. all others shoul have returned early.
		handled(notif.Command)
	}
}

这个方法将redis的消息解析为自定义的Notification类型的消息,判断Command的值,如果是 NoticePolicyChanged或NoticeSecretChanged,就会向realoadQueue中写入一个回调函数。
因为这里不需要用回调做任何事情,所以回调函数是nil。

  1. reloadQueueLoop
func (l *Load) reloadQueueLoop(cb ...func()) {
	for {
		select {
		case <-l.ctx.Done():
			return
		case fn := <-reloadQueue:
			requeueLock.Lock()
			requeue = append(requeue, fn)
			requeueLock.Unlock()
			log.Info("Reload queued")
			if len(cb) != 0 {
				cb[0]()
			}
		}
	}
}

reloadQueueLoop监听reloadQueue,当有新消息时,会实时将消息缓存到requeue切片中

  1. reloadLoop
func (l *Load) reloadLoop(complete ...func()) {
	ticker := time.NewTicker(1 * time.Second)
	for {
		select {
		case <-l.ctx.Done():
			return
		// We don't check for reload right away as the gateway peroms this on the
		// startup sequence. We expect to start checking on the first tick after the
		// gateway is up and running.
		case <-ticker.C:
			cb, ok := shouldReload()
			if !ok {
				continue
			}
			start := time.Now()
			l.DoReload()
			for _, c := range cb {
				// most of the callbacks are nil, we don't want to execute nil functions to
				// avoid panics.
				if c != nil {
					c()
				}
			}
			if len(complete) != 0 {
				complete[0]()
			}
			log.Infof("reload: cycle completed in %v", time.Since(start))
		}
	}
}

reloadLoop启动了一个定时器,每隔1s检查requeue切片是否为空,如果不为空,则调用l.DoReload,从iam-apiserver中拉取密钥和策略,缓存在内存中

2. 授权日志缓存机制

授权日志缓存机制是将日志写入到redis中,然后由另一个组件iam-pump去获取,供后续分析。
接下来看看授权日志缓存这一块,在初始化函数initialize中,会启动一个Analytics服务:

// start analytics service
	if s.analyticsOptions.Enable {
		analyticsStore := storage.RedisCluster{KeyPrefix: RedisKeyPrefix}
		analyticsIns := analytics.NewAnalytics(s.analyticsOptions, &analyticsStore)
		analyticsIns.Start()
	}

Analytics服务的启动与否是根据配置的Enable决定的。
NewAnalytics根据配置,创建了一个Analytics实例:

func NewAnalytics(options *AnalyticsOptions, store storage.AnalyticsHandler) *Analytics {
	ps := options.PoolSize
	recordsBufferSize := options.RecordsBufferSize
	workerBufferSize := recordsBufferSize / uint64(ps)
	log.Debug("Analytics pool worker buffer size", log.Uint64("workerBufferSize", workerBufferSize))

	recordsChan := make(chan *AnalyticsRecord, recordsBufferSize)

	analytics = &Analytics{
		store:                      store,
		poolSize:                   ps,
		recordsChan:                recordsChan,
		workerBufferSize:           workerBufferSize,
		recordsBufferFlushInterval: options.FlushInterval,
	}

	return analytics
}

上面代码创建了一个带缓冲的channel:recordsChan, 可以通过函数RecordHit向recordsChan写入数据:

func (r *Analytics) RecordHit(record *AnalyticsRecord) error {
	// check if we should stop sending records 1st
	if atomic.LoadUint32(&r.shouldStop) > 0 {
		return nil
	}

	// just send record to channel consumed by pool of workers
	// leave all data crunching and Redis I/O work for pool workers
	r.recordsChan <- record

	return nil
}

最后调用了Start()启动Analytics服务:

func (r *Analytics) Start() {
	r.store.Connect()

	// start worker pool
	atomic.SwapUint32(&r.shouldStop, 0)
	for i := 0; i < r.poolSize; i++ {
		r.poolWg.Add(1)
		go r.recordWorker()
	}
}

Analytics服务到这里就启动了N个recordWorker:

func (r *Analytics) recordWorker() {
	defer r.poolWg.Done()

	// this is buffer to send one pipelined command to redis
	// use r.recordsBufferSize as cap to reduce slice re-allocations
	recordsBuffer := make([][]byte, 0, r.workerBufferSize)

	// read records from channel and process
	lastSentTS := time.Now()
	for {
		var readyToSend bool
		select {
		case record, ok := <-r.recordsChan:
			// check if channel was closed and it is time to exit from worker
			if !ok {
				// send what is left in buffer
				r.store.AppendToSetPipelined(analyticsKeyName, recordsBuffer)

				return
			}

			// we have new record - prepare it and add to buffer

			if encoded, err := msgpack.Marshal(record); err != nil {
				log.Errorf("Error encoding analytics data: %s", err.Error())
			} else {
				recordsBuffer = append(recordsBuffer, encoded)
			}

			// identify that buffer is ready to be sent
			readyToSend = uint64(len(recordsBuffer)) == r.workerBufferSize

		case <-time.After(time.Duration(r.recordsBufferFlushInterval) * time.Millisecond):
			// nothing was received for that period of time
			// anyways send whatever we have, don't hold data too long in buffer
			readyToSend = true
		}

		// send data to Redis and reset buffer
		if len(recordsBuffer) > 0 && (readyToSend || time.Since(lastSentTS) >= recordsBufferForcedFlushInterval) {
			r.store.AppendToSetPipelined(analyticsKeyName, recordsBuffer)
			recordsBuffer = recordsBuffer[:0]
			lastSentTS = time.Now()
		}
	}
}

recordWorker从recordsChan中读取数据,并且用msgpack序列化,将其填充到切片recordsBuffer中。在最后追加到redis的pipeline,一次性将数据保存到redis,而且时间达到了预设的1s,也会进行同步。

最后再回到授权验证时记录授权结果的地方,iam-authz-server是使用ladon来进行授权的,因此是调用LogRejectedAccessRequestLogGrantedAccessRequest来记录的:

func (a *AuditLogger) LogRejectedAccessRequest(r *ladon.Request, p ladon.Policies, d ladon.Policies) {
	a.client.LogRejectedAccessRequest(r, p, d)
	log.Debug("subject access review rejected", log.Any("request", r), log.Any("deciders", d))
}

// LogGrantedAccessRequest write granted subject access to log.
func (a *AuditLogger) LogGrantedAccessRequest(r *ladon.Request, p ladon.Policies, d ladon.Policies) {
	a.client.LogGrantedAccessRequest(r, p, d)
	log.Debug("subject access review granted", log.Any("request", r), log.Any("deciders", d))
}

这是实现了ladon的Manager接口的方法,具体的授权记录是在authorizer中:

func (auth *Authorization) LogRejectedAccessRequest(r *ladon.Request, p ladon.Policies, d ladon.Policies) {
	var conclusion string
	if len(d) > 1 {
		allowed := joinPoliciesNames(d[0 : len(d)-1])
		denied := d[len(d)-1].GetID()
		conclusion = fmt.Sprintf("policies %s allow access, but policy %s forcefully denied it", allowed, denied)
	} else if len(d) == 1 {
		denied := d[len(d)-1].GetID()
		conclusion = fmt.Sprintf("policy %s forcefully denied the access", denied)
	} else {
		conclusion = "no policy allowed access"
	}

	rstring, pstring, dstring := convertToString(r, p, d)
	record := analytics.AnalyticsRecord{
		TimeStamp:  time.Now().Unix(),
		Username:   r.Context["username"].(string),
		Effect:     ladon.DenyAccess,
		Conclusion: conclusion,
		Request:    rstring,
		Policies:   pstring,
		Deciders:   dstring,
	}

	record.SetExpiry(0)
	_ = analytics.GetAnalytics().RecordHit(&record)
}

// LogGrantedAccessRequest write granted subject access to redis.
func (auth *Authorization) LogGrantedAccessRequest(r *ladon.Request, p ladon.Policies, d ladon.Policies) {
	conclusion := fmt.Sprintf("policies %s allow access", joinPoliciesNames(d))
	rstring, pstring, dstring := convertToString(r, p, d)
	record := analytics.AnalyticsRecord{
		TimeStamp:  time.Now().Unix(),
		Username:   r.Context["username"].(string),
		Effect:     ladon.AllowAccess,
		Conclusion: conclusion,
		Request:    rstring,
		Policies:   pstring,
		Deciders:   dstring,
	}

	record.SetExpiry(0)
	_ = analytics.GetAnalytics().RecordHit(&record)
}

最后都是调用了Analytics的RecordHit()向recordChan写入记录,最终模型如下图:
在这里插入图片描述

五. 总结

iam-authz-server的数据一致性架构如下图:
在这里插入图片描述

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值