31 Data Flow Designing Iamauthzserver and Looking at Data Flow Service Design

31 Data Flow Designing iamauthzserver and Looking at Data Flow Service Design #

Hello, I’m Kong Lingfei.

In Lesson 28 and Lesson 29, I introduced the design and implementation of IAM’s control flow service iam-apiserver. In this lesson, let’s take a look at the design and implementation of IAM’s data flow service iam-authz-server.

Since iam-authz-server is a data flow service with high performance requirements, it adopts some mechanisms to maximize the performance of API interfaces. In addition, in order to improve development efficiency and avoid reinventing the wheel, iam-authz-server shares most of the functional code with iam-apiserver. Next, let’s take a look at how iam-authz-server shares code with iam-apiserver and how it ensures the performance of API interfaces.

Introduction to iam-authz-server #

The current and only functionality of iam-authz-server is to provide resource authorization through the /v1/authz RESTful API interface. The /v1/authz interface utilizes github.com/ory/ladon to accomplish resource authorization.

As iam-authz-server handles requests for data flow, it is important to ensure that the API interface has high performance. To guarantee performance, iam-authz-server incorporates various caching techniques in its design.

Introduction to github.com/ory/ladon Package #

Since iam-authz-server utilizes github.com/ory/ladon for resource authorization, I will first introduce the github.com/ory/ladon package to help you better understand the authorization policies of iam-authz-server.

Ladon is a library written in Go language for implementing access control policies. It is similar to RBAC (Role-Based Access Control) and ACL (Access Control Lists), but compared to RBAC and ACL, Ladon can achieve more fine-grained access control and works in more complex environments (such as multi-tenancy, distributed applications, and large organizations).

Ladon solves the problem of who can/cannot perform which operations on which resources under certain conditions. To solve this problem, Ladon introduces authorization policies. An authorization policy is a document with a syntax specification that describes who can perform which operations on which resources under what conditions. Ladon can match the set authorization policies against the context of a request and determine whether the current authorization request passes. Below is an example of a Ladon authorization policy:

{
  "description": "One policy to rule them all.",
  "subjects": ["users:<peter|ken>", "users:maria", "groups:admins"],
  "actions" : ["delete", "<create|update>"],
  "effect": "allow",
  "resources": [
    "resources:articles:<.*>",
    "resources:printer"
  ],
  "conditions": {
    "remoteIP": {
        "type": "CIDRCondition",
        "options": {
            "cidr": "192.168.0.1/16"
        }
    }
  }
}

A policy consists of multiple elements used to describe specific authorization information. These elements can be seen as a set of rules. The core elements include subject, action, effect, resource, and condition. The reserved keywords for elements are case-insensitive, and there is no order requirement for describing them. For policies without specific constraint conditions, the condition element is optional. A policy contains the following six elements:

  • Subject: The subject is unique and represents an authorization subject. For example, “ken” or “printer-service.mydomain.com”.
  • Action: The action describes the permitted or denied operations.
  • Effect: The effect describes whether the policy produces an “allow” or “deny” result, including allow and deny.
  • Resource: The resource describes the specific data for authorization.
  • Condition: The condition describes the constraints under which the policy takes effect.
  • Description: The description of the policy.

With authorization policies, we can pass in the request context and let Ladon determine whether the request is authorized. Below is an example of a request:

{
  "subject": "users:peter",
  "action" : "delete",
  "resource": "resources:articles:ladon-introduction",
  "context": {
    "remoteIP": "192.168.0.5"
  }
}

As we can see, with the condition remoteIP="192.168.0.5", for the subject users:peter performing the delete action on the resource resources:articles:ladon-introduction, the effect of the authorization policy is allow. Therefore, Ladon will return the following result:

{
  "allowed": true
}

Ladon supports various conditions, as shown in the table below:

Image

As for how to use these conditions, you can refer to the Ladon Condition Usage Example. Additionally, Ladon also supports custom conditions.

Furthermore, Ladon supports authorization auditing to record authorization history. We can achieve this by attaching a ladon.AuditLogger to ladon.Ladon:

import "github.com/ory/ladon"
import manager "github.com/ory/ladon/manager/memory"

func main() {

    warden := ladon.Ladon{
        Manager: manager.NewMemoryManager(),
        AuditLogger: &ladon.AuditLoggerInfo{}
    }

    // ...
}

In the above example, we provide ladon.AuditLoggerInfo, which will print the called policy to standard error during authorization. AuditLogger is an interface defined as follows:

// AuditLogger tracks denied and granted authorizations.
type AuditLogger interface {
    LogRejectedAccessRequest(request *Request, pool Policies, deciders Policies)
    LogGrantedAccessRequest(request *Request, pool Policies, deciders Policies)
}

To implement a new AuditLogger, you only need to implement the AuditLogger interface. For example, we can implement an AuditLogger that saves authorization logs to Redis or MySQL.

Ladon supports tracking various authorization metrics, such as deny, allow, not match, error. You can handle these metrics by implementing the ladon.Metric interface. The ladon.Metric interface is defined as follows:

// Metric is used to expose metrics about authz
type Metric interface {
    // RequestDeniedBy is called when we get explicit deny by policy
    RequestDeniedBy(Request, Policy)
    // RequestAllowedBy is called when a matching policy has been found.
    RequestAllowedBy(Request, Policies)
    // RequestNoMatch is called when no policy has matched our request
    RequestNoMatch(Request)
    // RequestProcessingError is called when an unexpected error occurs
    RequestProcessingError(Request, Policy, error)
}

For example, by exposing these metrics to Prometheus, you can use the following example:

```go
type prometheusMetrics struct{}

func (mtr *prometheusMetrics) RequestDeniedBy(r ladon.Request, p ladon.Policy) {}
func (mtr *prometheusMetrics) RequestAllowedBy(r ladon.Request, policies ladon.Policies) {}
func (mtr *prometheusMetrics) RequestNoMatch(r ladon.Request) {}
func (mtr *prometheusMetrics) RequestProcessingError(r ladon.Request, err error) {}

func main() {

    warden := ladon.Ladon{
        Manager: manager.NewMemoryManager(),
        Metric:  &prometheusMetrics{},
    }

    // ...
}

When using Ladon, there are two things you need to pay attention to:

  • All checks are case-sensitive because the subject values can be case-sensitive IDs.
  • If Ladon cannot match a policy to a request, it will default to denying authorization and return an error.

Introduction to Using iam-authz-server #

Earlier, I introduced the resource authorization feature of iam-authz-server. Now, let’s learn how to use iam-authz-server, which involves calling the /v1/authz endpoint to complete the resource authorization. You can follow the three steps below to make a resource authorization request.

Step 1: Log in to iam-apiserver and create authorization policies and keys.

This step consists of three sub-steps.

  1. Log in to the iam-apiserver system and obtain an access token:
$ token=`curl -s -XPOST -H'Content-Type: application/json' -d'{"username":"admin","password":"Admin@2021"}' http://127.0.0.1:8080/login | jq -r .token`
  1. Create an authorization policy:
$ curl -s -XPOST -H"Content-Type: application/json" -H"Authorization: Bearer $token" -d'{"metadata":{"name":"authztest"},"policy":{"description":"One policy to rule them all.","subjects":["users:<peter|ken>","users:maria","groups:admins"],"actions":["delete","<create|update>"],"effect":"allow","resources":["resources:articles:<.*>","resources:printer"],"conditions":{"remoteIP":{"type":"CIDRCondition","options":{"cidr":"192.168.0.1/16"}}}}}' http://127.0.0.1:8080/v1/policies
  1. Create a key and extract the secretID and secretKey from the response:
$ curl -s -XPOST -H"Content-Type: application/json" -H"Authorization: Bearer $token" -d'{"metadata":{"name":"authztest"},"expires":0,"description":"admin secret"}' http://127.0.0.1:8080/v1/secrets
{"metadata":{"id":23,"name":"authztest","createdAt":"2021-04-08T07:24:50.071671422+08:00","updatedAt":"2021-04-08T07:24:50.071671422+08:00"},"username":"admin","secretID":"ZuxvXNfG08BdEMqkTaP41L2DLArlE6Jpqoox","secretKey":"7Sfa5EfAPIwcTLGCfSvqLf0zZGCjF3l8","expires":0,"description":"admin secret"}

Step 2: Generate a token to access iam-authz-server.

The jwt sign subcommand of iamctl can be used to issue a token based on the secretID and secretKey, making it easy to use.

$ iamctl jwt sign ZuxvXNfG08BdEMqkTaP41L2DLArlE6Jpqoox 7Sfa5EfAPIwcTLGCfSvqLf0zZGCjF3l8 # iamctl jwt sign $secretID $secretKey
eyJhbGciOiJIUzI1NiIsImtpZCI6Ilp1eHZYTmZHMDhCZEVNcWtUYVA0MUwyRExBcmxFNkpwcW9veCIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJpYW0uYXV0aHoubWFybW90ZWR1LmNvbSIsImV4cCI6MTYxNzg0NTE5NSwiaWF0IjoxNjE3ODM3OTk1LCJpc3MiOiJpYW1jdGwiLCJuYmYiOjE2MTc4Mzc5OTV9.za9yLM7lHVabPAlVQLCqXEaf8sTU6sodAsMXnmpXjMQ
    
You can use `iamctl jwt show <token>` to check the content of the token:

$ iamctl jwt show eyJhbGciOiJIUzI1NiIsImtpZCI6Ilp1eHZYTmZHMDhCZEVNcWtUYVA0MUwyRExBcmxFNkpwcW9veCIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJpYW0uYXV0aHoubWFybW90ZWR1LmNvbSIsImV4cCI6MTYxNzg0NTE5NSwiaWF0IjoxNjE3ODM3OTk1LCJpc3MiOiJpYW1jdGwiLCJuYmYiOjE2MTc4Mzc5OTV9.za9yLM7lHVabPAlVQLCqXEaf8sTU6sodAsMXnmpXjMQ
Header:
{
    "alg": "HS256",
    "kid": "ZuxvXNfG08BdEMqkTaP41L2DLArlE6Jpqoox",
    "typ": "JWT"
}
Claims:
{
    "aud": "iam.authz.marmotedu.com",
    "exp": 1617845195,
    "iat": 1617837995,
    "iss": "iamctl",
    "nbf": 1617837995
}

The token we generated contains the following information.

**Header**

  * alg: The algorithm used to generate the signature.
  * kid: The ID of the secret key.
  * typ: The type of the token, which is JWT.

**Claims**

  * aud: The recipient of the JWT token.
  * exp: The expiration time of the JWT token (in UNIX time format).
  * iat: The issuance time of the JWT token (in UNIX time format).
  * iss: The issuer of the token. Since we issued the token using the iamctl tool, the issuer here is iamctl.
  * nbf: The time from which the JWT token is valid (in UNIX time format), defaulting to the issuance time.

**Step 3, call the** `/v1/authz` **endpoint to complete the resource authorization request.**

The request method is as follows:

$ curl -s -XPOST -H'Content-Type: application/json' -H'Authorization: Bearer eyJhbGciOiJIUzI1NiIsImtpZCI6Ilp1eHZYTmZHMDhCZEVNcWtUYVA0MUwyRExBcmxFNkpwcW9veCIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJpYW0uYXV0aHoubWFybW90ZWR1LmNvbSIsImV4cCI6MTYxNzg0NTE5NSwiaWF0IjoxNjE3ODM3OTk1LCJpc3MiOiJpYW1jdGwiLCJuYmYiOjE2MTc4Mzc5OTV9.za9yLM7lHVabPAlVQLCqXEaf8sTU6sodAsMXnmpXjMQ' -d'{"subject":"users:maria","action":"delete","resource":"resources:articles:ladon-introduction","context":{"remoteIP":"192.168.0.5"}}' http://127.0.0.1:9090/v1/authz
{"allowed":true}

If the authorization is successful, it will return: `{"allowed":true}`. If the authorization fails, it will return:

{"allowed":false,"denied":true,"reason":"Request was denied by default"}

Implementation of iam-authz-server #

Next, let’s take a look at the specific implementation of iam-authz-server. I will explain it from four aspects: configuration processing, startup process, request handling process, and code architecture.

Configuration processing of iam-authz-server #

The main function of iam-authz-server is located in the authzserver.go file. You can read the code to understand the implementation of iam-authz-server. The service framework design of iam-authz-server is consistent with that of iam-apiserver. It also has three types of configurations: Options configuration, component configuration, and HTTP service configuration.

The Options configuration is defined in the options.go file:

type Options struct {
    RPCServer                string
    ClientCA                 string
    GenericServerRunOptions  *genericoptions.ServerRunOptions
    InsecureServing          *genericoptions.InsecureServingOptions
    SecureServing            *genericoptions.SecureServingOptions
    RedisOptions             *genericoptions.RedisOptions
    FeatureOptions           *genericoptions.FeatureOptions
    Log                      *log.Options
    AnalyticsOptions         *analytics.AnalyticsOptions
}

Compared with iam-apiserver, iam-authz-server has an additional AnalyticsOptions field, which is used to configure the Analytics service within iam-authz-server. The Analytics service asynchronously writes authorization logs to Redis.

iam-apiserver and iam-authz-server share the following configurations: GenericServerRunOptions, InsecureServing, SecureServing, FeatureOptions, RedisOptions, Log. Therefore, we can introduce many configuration items into the command-line parameters of iam-authz-server with just a few lines of code. This is a benefit of grouping command-line parameters: batch sharing.

Startup process of iam-authz-server #

Next, let’s take a detailed look at the startup process of iam-authz-server.

The startup process of iam-authz-server is similar to that of iam-apiserver. The major differences between them lie in the Options parameter configuration and application initialization content. In addition, compared to iam-apiserver, iam-authz-server only provides REST API services. The startup process is illustrated in the following diagram:

Request handling process of iam-authz-server’s RESTful API #

The request handling process of iam-authz-server is clear and standardized. The specific process is illustrated in the following diagram:

First, we make an API call (<HTTP Method> + <HTTP Request Path>) to the RESTful API interface provided by iam-authz-server: POST /v1/authz.

Next, after receiving the HTTP request, the Gin Web framework completes the request authentication through the authentication middleware. iam-authz-server uses Bearer authentication.

Then, the request is processed by a series of middleware that we have loaded, such as CORS, RequestID, and Dump middleware.

Finally, the routing is matched based on the <HTTP Method> + <HTTP Request Path>.

For example, if we request the RESTful API POST /v1/authz, the Gin Web framework will look for registered Controllers based on the HTTP Method and HTTP Request Path, and eventually match the authzController.Authorize Controller. In the Authorize Controller, the request parameters are parsed first, then the request parameters are validated, the business method for resource authorization is called, and finally the return result from the business layer is processed to return the final result of the HTTP request.

Code architecture of iam-authz-server #

The code design of iam-authz-server follows the same clean architecture design as iam-apiserver.

The code architecture of iam-authz-server is also divided into four layers: Models, Controller, Service, and Repository. The depth of the layers from Controller to Service to Repository gradually increases from left to right. The Models layer is independent of other layers and can be referenced by other layers. The diagram below illustrates the architecture:

There are three differences between the code architecture of iam-authz-server and iam-apiserver:

  • The iam-authz-server client does not support frontend and command-line interfaces.
  • The Repository layer of iam-authz-server interfaces with the iam-apiserver microservice instead of a database.
  • The business layer code of iam-authz-server is stored in the authorization directory.

Analysis of Key Code in iam-authz-server #

Like iam-apiserver, iam-authz-server also includes some excellent design ideas and key code. Let me introduce them one by one.

Resource Authorization #

Let’s first see how iam-authz-server implements resource authorization.

We can call the /v1/authz API interface of iam-authz-server to achieve access authorization for resources. The corresponding controller method for /v1/authz is 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)
}

This function uses the github.com/ory/ladon package to achieve resource access authorization. The authorization process is shown in the following diagram:

The specific steps are as follows:

  1. In the Authorize method, call c.ShouldBind(&r) to parse the API request parameters into a variable of type ladon.Request.

  2. Call the authorization.NewAuthorizer function, which will create and return a variable of type Authorizer that contains the Manager and AuditLogger fields.

    The Manager contains functions such as Create, Update, and FindRequestCandidates, which are used to perform CRUD operations on authorization policies. The AuditLogger contains functions like LogRejectedAccessRequest and LogGrantedAccessRequest, which are used to record denied and granted authorization requests as audit data.

  3. Call the auth.Authorize function to perform access authorization for the request. The content of the auth.Authorize function is as follows:

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,
    }
}

This function calls a.warden.IsAllowed(request) to complete the resource access authorization. The IsAllowed function will call FindRequestCandidates(r) to query the list of policies. Here, we only need to query the policy list of the requesting user. In the Authorize function, we store the username in the context of the ladon Request:

r.Context["username"] = c.GetHeader("username")

In the FindRequestCandidates function, we can retrieve the username from the Request and query the policy list in the cache based on the username. The implementation of FindRequestCandidates is as follows:

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
}

The code for the IsAllowed function is as follows:

func(v interface{}) {
    handleRedisEvent(v, nil, nil)
}

这个回调函数就是handleRedisEvent函数,它会处理订阅到的Redis事件。

  1. reloadQueueLoop协程

reloadQueueLoop函数从load包的reloadKeyQueue中读取reloadKey,并调用reloadKeys方法,将reloadKey放进reloadQueue中:

func reloadQueueLoop() {
    for {
        key := <-reloadKeyQueue
        reloadKeys([]string{key})
    }
}

reloadKeys方法就是将reloadKey放进reloadQueue中,是争对策略缓存的:

func reloadKeys(keys []string) {
    cacheKeyQueue <- keys
}

然后在reloadLoop协程中,会从reloadQueue中读取reloadKey,并调用load.loader的Reload方法来重新加载密钥和策略信息:

func reloadLoop() {
    for {
        keys := <-cacheKeyQueue
        l.lock.Lock()
        l.loader.Reload(keys...)
        l.lock.Unlock()
    }
}

上面这3个Go协程的运行,使得密钥和策略缓存可以实现及时更新。

下面我们来看下,启动Load服务的过程中,为什么要调用 l.DoReload() 完成一次密钥和策略的同步。

我们先来看下初始化一个Loader时,是如何同步密钥和策略信息到cache中的。

在initialize函数调用NewLoader方法创建Load实例时,传入了cache参数。

Load实例中保存了一个redis.Event,会在 StartPubSubHandler 的回调函数中保存redis事件信息:

// Reload 重新加载
func (r *EventWatcher) Reload() error {
    event := r.takeEvent()
    if event == nil {
        return nil
    }
    ...
}

在Reload方法中,会通过takeEvent方法获取redis事件,然后进行处理。

event是从Redis中取出的事件,它是包含了事件信息的结构体指针。

我们知道,在iam-apiserver中,每次导入、新建、更新、删除策略时,都会发布一个

HSET  'iam.targets' '<ID>' '<Policy>'

}

In the handleRedisEvent function, the message is parsed as a Notification type message and the value of Command is checked. If it is NoticePolicyChanged or NoticeSecretChanged, a callback function is written to the reloadQueue channel. Since we don’t need to do anything with the callback function here, it is nil. The reloadQueue is mainly used to inform the program that a key and policy synchronization needs to be completed.

  1. reloadQueueLoop goroutine

The reloadQueueLoop function listens for new messages (callback functions) written to the reloadQueue channel and caches the messages in the requeue slice in real-time, as shown in the following code:

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]()
            }
        }
    }
}
  1. reloadLoop goroutine

The reloadLoop function starts a timer that checks if the requeue slice is empty every 1 second. If it is not empty, the l.DoReload method is called to fetch the keys and policies from the iam-apiserver and cache them in memory.

The cache model for keys and policies is shown in the following diagram:

The specific process of caching keys and policies is as follows:

Receive upstream messages (in this case, from Redis) and cache the messages in a slice or buffered channel. Start a consumption goroutine to consume these messages. The consumption goroutine is reloadLoop, which checks if the requeue slice has a length other than 0 every 1 second. If it is not 0, it executes l.DoReload() to cache keys and policies.

Now, let’s take a look at the authorization log cache.

When starting the iam-authz-server, an Analytics service is also started, as shown in the following code (located in the internal/authzserver/server.go file):

if s.analyticsOptions.Enable {
    analyticsStore := storage.RedisCluster{KeyPrefix: RedisKeyPrefix}
    analyticsIns := analytics.NewAnalytics(s.analyticsOptions, &analyticsStore)
    analyticsIns.Start()
    s.gs.AddShutdownCallback(shutdown.ShutdownFunc(func(string) error {
        analyticsIns.Stop()
        return nil
    }))
}

The NewAnalytics function creates an Analytics instance based on the configuration:

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)

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

The above code creates a buffered recordsChan:

recordsChan := make(chan *AnalyticsRecord, recordsBufferSize)

The data type stored in recordsChan is AnalyticsRecord (defined here), and the buffer size is set to recordsBufferSize (specified by the --analytics.records-buffer-size option). The RecordHit function can be used to write data of type AnalyticsRecord to recordsChan:

func (r *Analytics) RecordHit(record *AnalyticsRecord) error {
    // check if we should stop sending records 1st
    // code omitted for brevity
}
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
}

The iam-authz-server logs authorization events by calling the LogGrantedAccessRequest and LogRejectedAccessRequest functions. When logging the authorization events, the function writes the logs to the recordsChan channel. The LogGrantedAccessRequest function, for example, creates an instance of the AnalyticsRecord struct and calls RecordHit to write the value of the variable to the recordsChan channel. By writing the authorization logs to the recordsChan channel instead of directly to Redis, it greatly reduces the write latency and the response delay of the API.

A worker process reads data from the recordsChan channel and writes the data to Redis in batches when a certain threshold is reached. In the Start function, a pool of workers is created with the number of workers specified by the --analytics.pool-size command line parameter. The Start function connects to the store, starts the worker pool, and also starts a goroutine to stop the analytics workers.

The recordWorker function reads the authorization logs from the recordsChan channel and stores them in a buffer called recordsBuffer. When the recordsBuffer is full or reaches the maximum delivery time, the function calls r.Store.AppendToSetPipelined(analyticsKeyName, recordsBuffer) to batch send the records to Redis. To improve transmission speed, the log content is encoded in msgpack format before transmission.

The caching mechanism described above can be abstracted into a caching model that satisfies the majority of asynchronous storing needs in practical development, as shown in the diagram below:

(Description of the diagram is cut-off:)

In order to maximize performance, the /v1/authz endpoint of iam-authz-server uses a lot of caching. However, this introduces the possibility of data inconsistency, as the data is stored in both persistent storage (Redis) and memory. Therefore, it is necessary to ensure consistency between the cached data and the database. The data consistency architecture is shown in the diagram above.

The synchronization process for keys and policies is as follows:

  1. The iam-webconsole sends a request to iam-apiserver to create (or update, delete) a key (or policy).
  2. When iam-apiserver receives a “write” request, it sends a PolicyChanged or SecretChanged message to the iam.cluster.notifications channel in Redis.
  3. When the Loader receives the message, it triggers the cache loader instance to execute the Reload method, which synchronizes the key and policy information from iam-apiserver.

The Loader does not care about the specific implementation of the Reload method; it only executes the Reload method when it receives the designated message. This allows for different cache strategies to be implemented.

In the Reload method of the cache instance, we actually call the List methods of the Secret and Policy repositories to retrieve the key and policy lists. The repository layer executes gRPC requests to get the key and policy lists from the iam-apiserver.

The Reload method of the cache places the retrieved key and policy lists in a cache of type ristretto, which is used by the business layer. The business layer code is located in the internal/authzserver/authorization directory.

Summary #

In this lecture, I introduced the design and implementation of the IAM data flow service iam-authz-server. iam-authz-server provides the /v1/authz RESTful API interface for third-party users to complete resource authorization functions, specifically using the Ladon package to achieve resource authorization. The Ladon package solves the problem of “under specific conditions, who can/cannot perform which operations on which resources.”

The configuration processing, startup process, and request processing flow of iam-authz-server are consistent with iam-apiserver. In addition, iam-authz-server also implements a concise architecture.

iam-authz-server improves the performance of the /v1/authz interface by caching key and policy information, as well as caching authorization logs.

When caching key and policy information, in order to keep consistent with the key and policy information in iam-apiserver, the Redis Pub/Sub mechanism is used. When there is a key/policy change in iam-apiserver, a message is published to the specified Redis channel. iam-authz-server subscribes to the same channel and when it receives a new message, it will parse the message and reacquire the key and policy information from iam-apiserver, caching them in memory.

After iam-authz-server completes resource authorization, it stores the authorization log in a buffered channel. There are multiple workers in the backend to consume the data in the channel and perform batch delivery. Batch delivery conditions can be set, such as the maximum number of logs to deliver and the maximum delivery time interval.

Exercises #

  1. The iam-authz-server and iam-apiserver share the application framework (including some configuration items) and code for the HTTP service framework. Please read the code of iam-authz-server to see how the IAM project achieves code reuse.

  2. The iam-authz-server uses ristretto to cache key and policy information. Please research other excellent cache packages available in the industry, and feel free to share in the comment section.

Feel free to leave a comment to discuss and exchange ideas. See you in the next lesson.