Auth0 x Snowflake: アプリケーションから透過的にSnowflakeに接続する方法
クラウド経営管理システムを提供する株式会社ログラス でエンジニアをしている村本 (@urmot2) です。
最近、Snowflakeをアプリケーションで活用できないか?という検討をしている中で、ログインしているユーザーの認証情報を使ってSnowflakeに接続することで、Snowflakeの権限周りの機能を使い倒せるのではないか?と思い色々と試して見ましたので、実践した内容をご紹介したいと思います。
※ この記事の内容は検証の域を出ておりませんのでご注意ください
SnowflakeとAuth0について
この記事では、SnowflakeやAuth0については一定知っている前提で書いています。
そのため、SnowflakeやAuth0についての説明は省かせて頂きます。
(わかりやすい記事が沢山出ておりますので、そちらをご参照ください 🙇♂️)
やりたいこと
「Auth0を使ってアプリケーションにログインしているユーザーの認証情報を使って、Snowflakeに接続すること」がこの記事でやりたいことです。
これができるようになるとどうなるかというと、ユーザーとSnowflake上のユーザーを1:1で管理できるようになります。
一般的にデータベースに接続する際にはUser/Passwordを利用することが多いと思います。
SnowflakeでもUser/Passwordで接続することが可能です。
しかし、User/Password形式だと、アプリケーションに対してUserが1つに決定されてしまうため、ユーザーレベルの権限制御はアプリケーション側で担う必要が出てきます。
SnowflakeのようなDWHのサービスでは、分析したい内容に合わせてテーブルを作成し、テーブルにアクセスできるユーザーを制限したいというニーズが生まれてきます。
もっと細かいものだと、ある条件のレコードにアクセスできるユーザーを制限したいという要望も出てくると思います。
SnowflakeではSQLで権限を付与することで、様々なアクセス制御をかけることができます。
Snowflake上のデータベースやテーブルに対して GRANT
句を使って、権限制御したり、行レベルのアクセス制御をかけることも可能になってきます。
これらを駆使することで、ユーザーに対するデータのアクセス制御をフルルクラッチで開発せずともSnowflake上に既にある機能で代替可能になるのではないか?というのが、検証しようと思ったキッカケでした。
SnowflakeにAuth0を使って接続する概要
まず、Snowflakeへ接続する時の認証方法は以下のようにいくつか存在します。
- User/Password
- SSO
- OAuth
- キーペア認証
- etc...
また、接続方法も複数用意されています。
- Pythonコネクタ
- Javaコネクト
- Node.jsドライバー
- Goドライバー
- ODBCドライバー
- JDBCドライバー
- etc...
今回はこの記事で利用するサンプルアプリケーションがJavaScriptなので、Node.jsドライバーを利用します。
接続時の認証にはOAuth接続を利用して、Auth0でログインしているユーザーのアクセストークンでアプリケーションからOAuthでSnowflakeに接続してみます。
ユーザー別の接続を実現したいだけであれば、UserごとにSnowflakeのパスワードを発行して、それを利用して接続する方法もありますが、パスワードの管理やローテーションなど考えることが多いので、今回は利用しない方向で進めます。
Snowflake側の設定
※ 事前に、Snowflakeのアカウントを作成する必要がございます
Snowflakeにユーザーを作成する
SnowflakeにAuth0のユーザーで接続するためには、Auth0のユーザーに対応するユーザーを事前にSnowflake上にも作っておく必要があります。
Snowflakeの設定画面からもユーザーを作成することはできますが、Snowflakeのコンソールから以下のSQLを実行することで、ユーザーを作成できます。
CREATE USER "ユーザーの一意な識別子" -- auth0のIDにしても良い
LOGIN_NAME = 'AUTH0|XXXXXXXXXXXXXXXXX' -- auth0のIDにする
DEFAULT_ROLE = 'SAMPLE_ROLE';
後ほど、利用するのでユーザーの LOGIN_NAME
の項目はAuth0のユーザーIDにしておきます。
デフォルトロールを設定しているように、ユーザーを作成する前にロールを作成しておく必要があります。
ロールにはユーザーがアクセスするDataBaseやWarehouseへのアクセス権限を付与しておきましょう。
CREATE ROLE SAMPLE_ROLE;
GRANT USAGE ON WAREHOUSE "{YOUR_WAREHOUSE_NAME}" TO ROLE SAMPLE_ROLE;
GRANT USAGE ON DATABASE "{YOUR_DB_NAME}" TO ROLE SAMPLE_ROLE;
GRANT USAGE ON SCHEMA "{YOUR_DB_NAME}".public TO ROLE SAMPLE_ROLE;
最後にユーザーにロールを付与しておきます。
GRANT ROLE SAMPLE_ROLE TO USER "ユーザーの一意な識別子";
Security Integrationの作成
SnowflakeにOAuth接続するためには事前にsecurity integrationというものをSnowflake上に設定しておく必要があります。
詳しくは以下のドキュメントに載っていますが、Auth0を利用する場合にはカスタム認証サーバーとして設定する必要があり、何を設定すれば良いか検討をつけるのがそこそこ難しかったです。
Snowflakeにログインし、コンソールで以下のSQLを実行するとSecurity Integrationを作成することができます。
CREATE SECURITY INTEGRATION auth0_oauth_integration
TYPE = external_oauth
ENABLED = true
EXTERNAL_OAUTH_TYPE = custom
EXTERNAL_OAUTH_ANY_ROLE_MODE = 'ENABLE_FOR_PRIVILEGE'
EXTERNAL_OAUTH_ISSUER = 'https://{your-domain}.auth0.com/' -- Auth0ドメイン
EXTERNAL_OAUTH_TOKEN_USER_MAPPING_CLAIM = 'sub'
EXTERNAL_OAUTH_SNOWFLAKE_USER_MAPPING_ATTRIBUTE = 'LOGIN_NAME'
EXTERNAL_OAUTH_SCOPE_MAPPING_ATTRIBUTE = 'scope'
EXTERNAL_OAUTH_SCOPE_DELIMITER = ' '
EXTERNAL_OAUTH_AUDIENCE_LIST = ('https://{your-api.com}')
EXTERNAL_OAUTH_JWS_KEYS_URL = 'https://{your-domain}.auth0.com/.well-known/jwks.json'; -- JWSキーのURL
各項目の詳しい説明はこちらのドキュメントに載っておりますので、割愛します。
このとき重要になってくるのが、 EXTERNAL_OAUTH_SNOWFLAKE_USER_MAPPING_ATTRIBUTE
を LOGIN_NAME
にすることです。
この項目では「Snowflake上のユーザーとマッピングする際にどの属性を使ってマッピングするか?」を設定しています。
また、 EXTERNAL_OAUTH_ANY_ROLE_MODE
という項目を ENABLE_FOR_PRIVILEGE
にしている場合には、「許可があるロール場合にはユーザーに対してロールを指定しなくても接続できる」という設定になります。
EXTERNAL_OAUTH_ANY_ROLE_MODE
を DISABLE
にしている場合には、トークンのスコープにロールを含める必要があります。
今回は面倒なので、トークンにロールを含めずとも接続できるように設定しています。
以下のようにロールに対して USE_ANY_ROLE
権限を付与しておきます。
GRANT USE_ANY_ROLE ON INTEGRATION auth0_oauth_integration TO SAMPLE_ROLE;
Snowflake側の設定はこれで終わりです。
Auth0側の設定
Auth0ではアプリケーションのログインに利用するApplicationの設定と、Snowflake用のAPIを作成する必要があります。
※ 事前にAuth0のアカウントを作成する必要がございます
サンプルアプリケーションの作成
ここで利用するアプリケーションは何でも良いです。
今回は、以下のQuickStartからReactのサンプルアプリケーションを作成しておきましょう。
Create New ApplicationでApplicationを作成し、右上のDownload sampleを押すと簡単にサンプルアプリケーションをダウンロードすることができます。
Snowflake用のAPIの設定
Snowflake用のアクセストークンを取得するためにAuth0上にAPIを作成しておきましょう。
この時、一つ注意するポイントとしては ANY_ROLE_MODE
を使ってログインする際にはアクセストークンのスコープに session:role-any
という項目が含まれていなければなりません。
(詳しくは以下のドキュメントをご参照ください)
アクセストークンのスコープに session:role-any
を設定するために、APIのPermissionsに session:role-any
を追加しておきます。
ここで設定したPermissionをSnowflakeにログインするユーザーに対して付与することも忘れずにやっておきましょう。
Auth0の設定は以上になります。
アプリケーションの修正
フロントエンドの修正
サンプルアプリケーションを少し修正して、Snowflake用のトークンを取得できるようにしましょう。
ExternalApi.js
の callApi
で getAccessTokenSilently
をしている箇所に、audience
と scope
の設定を追加します。
この設定により、ログインしたユーザーに session:role-any
のPermissionが付与されていれば、Access Tokenの scope
に session:role-any
が付与されるようになります。
const callApi = async () => {
try {
const token = await getAccessTokenSilently({
/* authorizationParamsを追加 */
authorizationParams: {
audience: 'https://{your-domain}.snowflakecomputing.com',
scope: 'session:role-any'
}
})
const response = await fetch(`${apiOrigin}/api/external`, {
headers: {
Authorization: `Bearer ${token}`,
},
});
const responseData = await response.json();
setState({
...state,
showResult: true,
apiMessage: responseData,
});
} catch (error) {
setState({
...state,
error: error.error,
});
}
};
おそらく、APIを実行する際に Consent Required
というエラーに直面することになると思います。
このサンプルアプリケーションはそこもカバーしているので、 handleConsent
も以下のように authorizationParam
を設定しておきましょう。
const handleConsent = async () => {
try {
/* authorizationParam を設定する */
await getAccessTokenWithPopup({
authorizationParams: {
audience: 'https://{your-domain}.snowflakecomputing.com',
scope: 'session:role-any'
}
});
setState({
...state,
error: null,
});
} catch (error) {
setState({
...state,
error: error.error,
});
}
await callApi();
};
こうすることで以下のようなワーニングが出てきたときに、ポップアップで同意を得ることができます。
フロントエンドの修正は以上です。
サーバーサイドの修正
さて、サーバーサイドを修正して、フロントで取得したアクセストークンを利用して、SnowflakeにOAuth接続してみます。
最初に書いたようにSnowflakeのNode.jsドライバーを利用して、サーバーからSnowflakeに接続します。
まず、 snowflake-sdk
をインストールしましょう。
npm install snowflake-sdk
api-server.js
を開いて snowflake-sdk
をインポートしておきます。
const snowflake = require('snowflake-sdk');
一つだけエンドポイントが作成されているので、そのエンドポイントの中身を変更してsnowflakeへの接続を取得し、SQLを実行してみましょう。
Snowflakeにテスト用のテーブルを作成し、データを流し込んでおくと検証がしやすいです。
(作成したテーブルのREAD権限をRoleに対して付与することもお忘れなく...)
app.get("/api/external", checkJwt, (req, res) => {
// コネクションオブジェクトを作成
const connection = snowflake.createConnection({
account: 'your-account-id',
authenticator: 'OAUTH',
token: req.auth.token,
warehouse: '{YOUR_WAREHOUSE_NAME}',
database: '{YOUR_DATABASE_NAME}',
schema: 'PUBLIC',
});
// 接続する
await connection.connectAsync((err, conn) => {
if (err) {
console.error(err)
} else {
console.log('Successfully connected to Snowflake')
}
})
// SQLを実行する
const rows = await new Promise((resolve, reject) => {
connection.execute({
sqlText: 'SELECT * FROM "{YOUR_TABLE}"',
complete: (err, stmt, rows) => {
if (err) {
reject(err);
} else {
resolve(rows);
}
}
});
}).catch(console.error);
console.log(rows);
res.send({
msg: `result: ${JSON.stringify(rows)}`,
});
connection.destroy(() => {});
});
これでサーバーサイド側の設定は終わりです。
実際に、サンプルアプリケーションにログインし、Exteral APIから「Ping API」ボタンをクリックすると、Auth0からアクセストークンを取得し、サーバーにアクセストークンを送り、そのアクセストークンを利用して、Snowflakeに接続し、SQLを実行されると思います。
まとめ
Auth0でログインしているユーザーのアクセストークンを利用してSnowflakeに接続する方法をサンプルアプリケーションを使って実現する方法を解説しました。
Auth0のユーザーに対応するSnowflakeユーザーを作成して、適切な権限を付与することで、透過的にSnowflakeに接続しつつ、Snowflakeのアクセス制限機能を使い倒すことができそうです。
一方で、リクエストの度にコネクションを作成する必要があるなど、オーバーヘッドも一定あることが見えてきました。
その他にも、アプリケーションでSnowflakeを利用するには、パフォーマンスやコスト面など考えることが沢山ありそうです。
Discussion