GemFireのSecurity Managerを実装して認証・認可を試す

VMware Gemfire（旧Tanzu GemFire）はデータへのアクセスやGemFireのリソースへのアクセスの制御をRBACにて実現することが出来る。
RBACを使うためにはSeucurity Managerを実装する必要があるが、Built-inで[ExampleSecurityManager](https://geode.apache.org/releases/latest/javadoc/org/apache/geode/examples/security/ExampleSecurityManager.html)が含まれているため、これを使うことも出来る。
しかし、触ってみたところ思ったとおりに動かないところがあり、代わりにこちらの[GemFire Security Manager Basics: Authentication/Authorization](https://gemfire.dev/blog/gemfire-security-manager-basics-authentication/authorization/)というブログのソースコードを利用し、Security Managerを実装した時のメモ。

## 前準備
依存パッケージを[Pivotal Commercial Maven Repository](https://commercial-repo.pivotal.io/login/auth)から引っ張るため、こちらのアカウントを作成する。
アカウント作成後、`${HOME}/.m2/settings.xml`に上記のリポジトリの認証情報を設定する。
```xml
    <settings>
        <servers>
            <server>
                <id>gemfire-release-repo</id>
                <username>myaccount@mydomain</username>
                <password>YOUR_PASSWORD</password>
            </server>
        </servers>
    </settings>
```

## 実装
今回以下のようなファイルを作成する。
```
.
├── pom.xml
└── src
    └── main
        └── java
            └── com
                └── vmware
                    └── gemfire
                        ├── BasicSecurityManager.java
                        └── User.java
```
RBACを制御する`BasicSecurityManager.java`とユーザの定義を管理する`User.java`からなる。

`User.java`の実装は以下となる。
<details><summary>User.java（クリックすると表示）</summary><div>

```java
package com.vmware.gemfire;

import java.io.Serializable;
import java.util.List;

import org.apache.geode.security.ResourcePermission;


public class User implements Serializable {

   List<ResourcePermission> userPermissions;
   String userName;
   String userPassword;

   public User(String userName, String userPassword, List<ResourcePermission> userPermissions) {
      this.userName = userName;
      this.userPassword = userPassword;
      this.userPermissions = userPermissions;
   }

   public String getUserPassword() {
      return userPassword;
   }

   @Override
   public String toString() {
      return userName;
   }

   public List<ResourcePermission> getPermissions() {
      return this.userPermissions;
   }

   public boolean hasPermission(ResourcePermission resourcePermissionRequested) {
      boolean hasPermission = false;

      for (ResourcePermission userPermission : userPermissions) {
         if (userPermission.implies(resourcePermissionRequested)) {
            hasPermission = true;
            break;
         }
      }
      return hasPermission;
   }
}
```

</div></details>

RBACで権限を付与するため、`User`は`userName`、`userPassword`、`userPermissions`の項目を持つ。
各項目はSecurityManager側で値を設定する。

SecurityManagerとなる`BasicSecurityManager.java`の実装は以下となる。
<details><summary>BasicSecurityManager.java（クリックすると表示）</summary><div>

```java
package com.vmware.gemfire;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Properties;

import org.apache.geode.security.AuthenticationFailedException;
import org.apache.geode.security.ResourcePermission;
import org.apache.geode.security.SecurityManager;

public class BasicSecurityManager implements SecurityManager {

   private HashMap<String, User> approvedUsersList = new HashMap<>();

   @Override
   public void init(final Properties securityProperties) {

      List<ResourcePermission> operatorPermissions = new ArrayList<>();
      operatorPermissions.add(new ResourcePermission(ResourcePermission.Resource.CLUSTER,
              ResourcePermission.Operation.MANAGE));
      operatorPermissions.add(new ResourcePermission(ResourcePermission.Resource.CLUSTER,
              ResourcePermission.Operation.WRITE));
      operatorPermissions.add(new ResourcePermission(ResourcePermission.Resource.CLUSTER,
              ResourcePermission.Operation.READ));

      User operator = new User("operator", "secret", operatorPermissions);

      List<ResourcePermission> appDevPermissions = new ArrayList<>();
      appDevPermissions.add(new ResourcePermission(ResourcePermission.Resource.CLUSTER,
              ResourcePermission.Operation.READ));
      appDevPermissions.add(new ResourcePermission(ResourcePermission.Resource.DATA,
              ResourcePermission.Operation.MANAGE));
      appDevPermissions.add(new ResourcePermission(ResourcePermission.Resource.DATA,
              ResourcePermission.Operation.WRITE));
      appDevPermissions.add(new ResourcePermission(ResourcePermission.Resource.DATA,
              ResourcePermission.Operation.READ));

      User appDeveloper = new User("appDeveloper", "NotSoSecret", appDevPermissions);

      this.approvedUsersList.put("operator", operator);
      this.approvedUsersList.put("appDeveloper", appDeveloper);

   }
   @Override
   public Object authenticate(Properties credentials) throws AuthenticationFailedException {

      String usernamePassedIn = credentials.getProperty(USER_NAME);
      String passwordPassedIn = credentials.getProperty(PASSWORD);

      User authenticatedUser = this.approvedUsersList.get(usernamePassedIn);

      if (authenticatedUser == null) {
         throw new AuthenticationFailedException("Wrong username/password");
      }

      if (authenticatedUser != null && !authenticatedUser.getUserPassword().equals(passwordPassedIn)
              && !"".equals(authenticatedUser)) {
         throw new AuthenticationFailedException("Wrong username/password");
      }

      return authenticatedUser;
   }

   @Override
   public boolean authorize(Object principal, ResourcePermission resourcePermissionRequested) {

      if (principal == null) {
         return false;
      }

      User user = this.approvedUsersList.get(principal.toString());

      if (user == null) {
         return false;
      }

      for (ResourcePermission userPermission : user.getPermissions()) {
         if (userPermission.implies(resourcePermissionRequested)) {
            return true;
         }
      }

      return false;
   }
}
```

</div></details>

`init`では管理者権限である`operatorPermissions`と開発者権限である`appDevPermissions`に以下の権限を設定している。
- `operatorPermissions`:`CLUSTER:MANGE`/`CLUSTER:WRITE`/`CLUSTER:READ`
- `appDevPermissions`:`CLUSTER:READ`/`DATA:MANGE`/`DATA:WRITE`/`DATA:READ`

LocatorやServerの操作は管理者権限に付与し、Regionの操作などを開発者権限に付与している。
どの権限がどの操作と紐づくかの詳細は[こちら](https://docs.vmware.com/en/VMware-GemFire/10.0/gf/managing-security-implementing_authorization.html)を参照のこと。
また、初期ユーザとして以下を作成している。
|ユーザ名|パスワード|
|---|---|
|operator|secret|
|appDeveloper|NotSoSecret|

`authenticate`では認証の部分を実装し、リストに存在するユーザかをチェックした後に、パスワードが一致するかのチェックを行っている。
`authorize`では認可の部分を実装し、権限を付与している。

最後に、`pom.xml`は以下のようになる。

<details><summary>pom.xml（クリックすると表示）</summary><div>

```xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	
	<groupId>com.vmware.gemfire</groupId>
	<artifactId>BasicSecurityManager</artifactId>
	<packaging>jar</packaging>
	<version>1.0-SNAPSHOT</version>

	<properties>
		<maven.compiler.source>1.8</maven.compiler.source>
		<maven.compiler.target>1.8</maven.compiler.target>
	</properties>
	<repositories>
		<repository>
			<id>gemfire-release-repo</id>
			<name>Pivotal GemFire Release Repository</name>
			<url>https://commercial-repo.pivotal.io/data3/gemfire-release-repo/gemfire</url>
		</repository>
	</repositories>
	<dependencies>
		<dependency>
			<groupId>com.vmware.gemfire</groupId>
			<artifactId>gemfire-core</artifactId>
			<version>10.0.0</version>
		</dependency>
	</dependencies>
</project>
```

</div></details>

依存パッケージの取り込みのために`gemfire-release-repo`を書いている。
公式ドキュメントの記載は[こちら](https://docs.vmware.com/en/VMware-GemFire/10.0/gf/getting_started-installation-obtain_gemfire_maven.html)になる。

## 検証
先程のコードをビルドする。
```sh
mvn clean package
export JAR_DIR=${PWD}/target
```
検証用ディレクトリを作成し、Locatorを起動する。
```sh
mkdir /tmp/gemauth
cd /tmp/gemauth
gfsh -e "start locator --name locator1 --J=-Dgemfire.security-manager=com.vmware.gemfire.BasicSecurityManager --classpath=${JAR_DIR}/BasicSecurityManager-1.0-SNAPSHOT.jar"
```
起動に成功すると、以下のようなメッセージが出る。
```
Unable to auto-connect (Security Manager may be enabled). Please use "connect --locator=192.168.0.22[10334] --user --password" to connect Gfsh to the locator.

Authentication required to connect to the Manager.
```
次に`operator`でServerを起動する。
```sh
gfsh -e "start server --classpath=${JAR_DIR}/BasicSecurityManager-1.0-SNAPSHOT.jar --name server1 --locators 192.168.0.22[10334] --user operator --password secret"
```
問題なく成功するはずだ。
試しに`appDeveloper`でも起動してみる。
```sh
gfsh -e "start server --classpath=${JAR_DIR}/BasicSecurityManager-1.0-SNAPSHOT.jar --name server2 --locators 192.168.0.22[10334] --user appDeveloper --password NotSoSecret --server-port 40405"
```
実行すると、以下のように権限不足でエラーとなる。
```
Exception in thread "main" com.vmware.gemfire.deployment.modules.internal.LauncherException: org.apache.geode.security.GemFireSecurityException: com.vmware.gemfire.tcpmessenger.internal.HandshakeFailedException: Security check failed. appDeveloper not authorized for CLUSTER:MANAGE
```
次にデータを作成する。最初に`operator`で実行する。
```sh
gfsh -e "connect --locator 192.168.0.22[10334] --user operator --password secret" -e "create region --name=regionA --type=REPLICATE"
```
以下のようなエラーとなる。
```
Unauthorized. Reason : operator not authorized for DATA:MANAGE
```
`operator`にはDATA関連の権限を渡していないためエラーとなった。
```sh
gfsh -e "connect --locator 192.168.0.22[10334] --user appDeveloper --password NotSoSecret" -e "create region --name=regionA --type=REPLICATE"
```
こちらは以下のように成功した。
```
Member  | Status | Message
------- | ------ | --------------------------------------
server1 | OK     | Region "/regionA" created on "server1"

Cluster configuration for group 'cluster' is updated.
```
putも問題なく出来る。
```sh
gfsh -e "connect --locator 192.168.0.22[10334] --user appDeveloper --password NotSoSecret" -e 'put --region=regionA --key="1" --value="one"' -e 'put --region=regionA --key="2" --value="two"'
```

## まとめ
RBACを実装するのは面倒だが、カスタマイズの観点では非常に柔軟な実装が出来るのは利点である。
Regionごとに権限を分けるようなことも出来、きめ細かな権限設定をしたい場合にはここで紹介した方法を使うとよいかと思う。