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是用来记录审计的,包含LogRejectedAccessRequest
和LogGrantedAccessRequest
函数,记录被拒绝的授权请求和被允许的授权请求。
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中这一部分的接口设计,我们从授权这一块入手:
- 首先在创建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中获取密钥和策略
- 接着将这个
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方法,这个方法上面已经讨论过,就是授权的实现。
- 最后看授权时是怎么拿到缓存的数据的,在
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。
- 最后看看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具体做了啥
- 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), ¬if); 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。
- 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切片中
- 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来进行授权的,因此是调用LogRejectedAccessRequest
和LogGrantedAccessRequest
来记录的:
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的数据一致性架构如下图: