@@ -4,10 +4,16 @@ import HiveDriverError from '../../../errors/HiveDriverError';
4
4
import { LogLevel } from '../../../contracts/IDBSQLLogger' ;
5
5
import OAuthToken from './OAuthToken' ;
6
6
import AuthorizationCode from './AuthorizationCode' ;
7
- import { OAuthScope , OAuthScopes } from './OAuthScope' ;
7
+ import { OAuthScope , OAuthScopes , scopeDelimiter } from './OAuthScope' ;
8
8
import IClientContext from '../../../contracts/IClientContext' ;
9
9
10
+ export enum OAuthFlow {
11
+ U2M = 'U2M' ,
12
+ M2M = 'M2M' ,
13
+ }
14
+
10
15
export interface OAuthManagerOptions {
16
+ flow : OAuthFlow ;
11
17
host : string ;
12
18
callbackPorts ?: Array < number > ;
13
19
clientId ?: string ;
@@ -47,9 +53,7 @@ export default abstract class OAuthManager {
47
53
48
54
protected abstract getCallbackPorts ( ) : Array < number > ;
49
55
50
- protected getScopes ( requestedScopes : OAuthScopes ) : OAuthScopes {
51
- return requestedScopes ;
52
- }
56
+ protected abstract getScopes ( requestedScopes : OAuthScopes ) : OAuthScopes ;
53
57
54
58
protected async getClient ( ) : Promise < BaseClient > {
55
59
// Obtain http agent each time when we need an OAuth client
@@ -113,17 +117,11 @@ export default abstract class OAuthManager {
113
117
if ( ! accessToken || ! refreshToken ) {
114
118
throw new Error ( 'Failed to refresh token: invalid response' ) ;
115
119
}
116
- return new OAuthToken ( accessToken , refreshToken ) ;
120
+ return new OAuthToken ( accessToken , refreshToken , token . scopes ) ;
117
121
}
118
122
119
- private async refreshAccessTokenM2M ( ) : Promise < OAuthToken > {
120
- const { access_token : accessToken , refresh_token : refreshToken } = await this . getTokenM2M ( ) ;
121
-
122
- if ( ! accessToken ) {
123
- throw new Error ( 'Failed to fetch access token' ) ;
124
- }
125
-
126
- return new OAuthToken ( accessToken , refreshToken ) ;
123
+ private async refreshAccessTokenM2M ( token : OAuthToken ) : Promise < OAuthToken > {
124
+ return this . getTokenM2M ( token . scopes ?? [ ] ) ;
127
125
}
128
126
129
127
public async refreshAccessToken ( token : OAuthToken ) : Promise < OAuthToken > {
@@ -137,10 +135,16 @@ export default abstract class OAuthManager {
137
135
throw error ;
138
136
}
139
137
140
- return this . options . clientSecret === undefined ? this . refreshAccessTokenU2M ( token ) : this . refreshAccessTokenM2M ( ) ;
138
+ switch ( this . options . flow ) {
139
+ case OAuthFlow . U2M :
140
+ return this . refreshAccessTokenU2M ( token ) ;
141
+ case OAuthFlow . M2M :
142
+ return this . refreshAccessTokenM2M ( token ) ;
143
+ // no default
144
+ }
141
145
}
142
146
143
- private async getTokenU2M ( scopes : OAuthScopes ) {
147
+ private async getTokenU2M ( scopes : OAuthScopes ) : Promise < OAuthToken > {
144
148
const client = await this . getClient ( ) ;
145
149
146
150
const authCode = new AuthorizationCode ( {
@@ -153,37 +157,47 @@ export default abstract class OAuthManager {
153
157
154
158
const { code, verifier, redirectUri } = await authCode . fetch ( mappedScopes ) ;
155
159
156
- return client . grant ( {
160
+ const { access_token : accessToken , refresh_token : refreshToken } = await client . grant ( {
157
161
grant_type : 'authorization_code' ,
158
162
code,
159
163
code_verifier : verifier ,
160
164
redirect_uri : redirectUri ,
161
165
} ) ;
166
+
167
+ if ( ! accessToken ) {
168
+ throw new Error ( 'Failed to fetch access token' ) ;
169
+ }
170
+ return new OAuthToken ( accessToken , refreshToken , mappedScopes ) ;
162
171
}
163
172
164
- private async getTokenM2M ( ) {
173
+ private async getTokenM2M ( scopes : OAuthScopes ) : Promise < OAuthToken > {
165
174
const client = await this . getClient ( ) ;
166
175
176
+ const mappedScopes = this . getScopes ( scopes ) ;
177
+
167
178
// M2M flow doesn't really support token refreshing, and refresh should not be available
168
179
// in response. Each time access token expires, client can just acquire a new one using
169
180
// client secret. Here we explicitly return access token only as a sign that we're not going
170
181
// to use refresh token for M2M flow anywhere later
171
182
const { access_token : accessToken } = await client . grant ( {
172
183
grant_type : 'client_credentials' ,
173
- scope : 'all-apis' , // this is the only allowed scope for M2M flow
184
+ scope : mappedScopes . join ( scopeDelimiter ) ,
174
185
} ) ;
175
- return { access_token : accessToken , refresh_token : undefined } ;
176
- }
177
-
178
- public async getToken ( scopes : OAuthScopes ) : Promise < OAuthToken > {
179
- const { access_token : accessToken , refresh_token : refreshToken } =
180
- this . options . clientSecret === undefined ? await this . getTokenU2M ( scopes ) : await this . getTokenM2M ( ) ;
181
186
182
187
if ( ! accessToken ) {
183
188
throw new Error ( 'Failed to fetch access token' ) ;
184
189
}
190
+ return new OAuthToken ( accessToken , undefined , mappedScopes ) ;
191
+ }
185
192
186
- return new OAuthToken ( accessToken , refreshToken ) ;
193
+ public async getToken ( scopes : OAuthScopes ) : Promise < OAuthToken > {
194
+ switch ( this . options . flow ) {
195
+ case OAuthFlow . U2M :
196
+ return this . getTokenU2M ( scopes ) ;
197
+ case OAuthFlow . M2M :
198
+ return this . getTokenM2M ( scopes ) ;
199
+ // no default
200
+ }
187
201
}
188
202
189
203
public static getManager ( options : OAuthManagerOptions ) : OAuthManager {
@@ -245,6 +259,14 @@ export class DatabricksOAuthManager extends OAuthManager {
245
259
protected getCallbackPorts ( ) : Array < number > {
246
260
return this . options . callbackPorts ?? DatabricksOAuthManager . defaultCallbackPorts ;
247
261
}
262
+
263
+ protected getScopes ( requestedScopes : OAuthScopes ) : OAuthScopes {
264
+ if ( this . options . flow === OAuthFlow . M2M ) {
265
+ // this is the only allowed scope for M2M flow
266
+ return [ OAuthScope . allAPIs ] ;
267
+ }
268
+ return requestedScopes ;
269
+ }
248
270
}
249
271
250
272
export class AzureOAuthManager extends OAuthManager {
@@ -273,7 +295,18 @@ export class AzureOAuthManager extends OAuthManager {
273
295
protected getScopes ( requestedScopes : OAuthScopes ) : OAuthScopes {
274
296
// There is no corresponding scopes in Azure, instead, access control will be delegated to Databricks
275
297
const tenantId = this . options . azureTenantId ?? AzureOAuthManager . datatricksAzureApp ;
276
- const azureScopes = [ `${ tenantId } /user_impersonation` ] ;
298
+
299
+ const azureScopes = [ ] ;
300
+
301
+ switch ( this . options . flow ) {
302
+ case OAuthFlow . U2M :
303
+ azureScopes . push ( `${ tenantId } /user_impersonation` ) ;
304
+ break ;
305
+ case OAuthFlow . M2M :
306
+ azureScopes . push ( `${ tenantId } /.default` ) ;
307
+ break ;
308
+ // no default
309
+ }
277
310
278
311
if ( requestedScopes . includes ( OAuthScope . offlineAccess ) ) {
279
312
azureScopes . push ( OAuthScope . offlineAccess ) ;
0 commit comments