@metaplay/metaplay-auth 1.2.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/stackapi.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  import { isValidFQDN, getGameserverAdminUrl } from './utils.js'
2
2
  import { logger } from './logging.js'
3
+ import { dump } from 'js-yaml'
4
+ import { getUserinfo } from './auth.js'
3
5
 
4
6
  interface AwsCredentialsResponse {
5
7
  AccessKeyId: string
@@ -8,34 +10,84 @@ interface AwsCredentialsResponse {
8
10
  Expiration: string
9
11
  }
10
12
 
11
- interface GameserverId {
13
+ export interface GameserverId {
12
14
  gameserver?: string
13
15
  organization?: string
14
16
  project?: string
15
17
  environment?: string
16
18
  }
17
19
 
20
+ interface KubeConfig {
21
+ apiVersion?: string
22
+ kind: string
23
+ 'current-context': string
24
+ clusters: KubeConfigCluster[]
25
+ contexts: KubeConfigContext[]
26
+ users: KubeConfigUser[]
27
+ preferences?: any
28
+ }
29
+
30
+ interface KubeConfigCluster {
31
+ cluster: {
32
+ 'certificate-authority-data': string
33
+ server: string
34
+ }
35
+ name: string
36
+ }
37
+
38
+ interface KubeConfigContext {
39
+ context: {
40
+ cluster: string
41
+ user: string
42
+ namespace?: string
43
+ }
44
+ name: string
45
+ }
46
+
47
+ interface KubeConfigUser {
48
+ name: string
49
+ user: {
50
+ token?: string
51
+ exec?: {
52
+ command: string
53
+ args: string[]
54
+ apiVersion: string
55
+ }
56
+ }
57
+ }
58
+
59
+ interface KubeExecCredential {
60
+ apiVersion: string
61
+ kind: string
62
+ spec: {
63
+ cluster?: {
64
+ server: string
65
+ certificateAuthorityData: string
66
+ }
67
+ }
68
+ status: {
69
+ token: string
70
+ expirationTimestamp: string
71
+ }
72
+ }
73
+
18
74
  export class StackAPI {
19
75
  private readonly accessToken: string
20
76
 
21
- private _stack_api_base_uri: string
77
+ private readonly _stackApiBaseUrl: string
22
78
 
23
- constructor (accessToken: string) {
79
+ constructor (accessToken: string, stackApiBaseUrl: string | undefined) {
24
80
  if (accessToken == null) {
25
81
  throw new Error('accessToken must be provided')
26
82
  }
27
83
 
28
84
  this.accessToken = accessToken
29
- this._stack_api_base_uri = 'https://infra.p1.metaplay.io/stackapi'
30
- }
31
-
32
- get stack_api_base_uri (): string {
33
- return this._stack_api_base_uri.replace(/\/$/, '')
34
- }
35
85
 
36
- set stack_api_base_uri (uri: string) {
37
- uri = uri.replace(/\/$/, '') // Remove trailing slash
38
- this._stack_api_base_uri = uri
86
+ if (stackApiBaseUrl) {
87
+ this._stackApiBaseUrl = stackApiBaseUrl.replace(/\/$/, '') // Remove trailing slash
88
+ } else {
89
+ this._stackApiBaseUrl = 'https://infra.p1.metaplay.io/stackapi'
90
+ }
39
91
  }
40
92
 
41
93
  async getAwsCredentials (gs: GameserverId): Promise<AwsCredentialsResponse> {
@@ -45,10 +97,10 @@ export class StackAPI {
45
97
  const adminUrl = getGameserverAdminUrl(gs.gameserver)
46
98
  url = `https://${adminUrl}/.infra/credentials/aws`
47
99
  } else {
48
- url = `${this.stack_api_base_uri}/v0/credentials/${gs.gameserver}/aws`
100
+ url = `${this._stackApiBaseUrl}/v0/credentials/${gs.gameserver}/aws`
49
101
  }
50
102
  } else if (gs.organization != null && gs.project != null && gs.environment != null) {
51
- url = `${this.stack_api_base_uri}/v1/servers/${gs.organization}/${gs.project}/${gs.environment}/credentials/aws`
103
+ url = `${this._stackApiBaseUrl}/v1/servers/${gs.organization}/${gs.project}/${gs.environment}/credentials/aws`
52
104
  } else {
53
105
  throw new Error('Invalid arguments for getAwsCredentials')
54
106
  }
@@ -76,10 +128,10 @@ export class StackAPI {
76
128
  const adminUrl = getGameserverAdminUrl(gs.gameserver)
77
129
  url = `https://${adminUrl}/.infra/credentials/k8s`
78
130
  } else {
79
- url = `${this.stack_api_base_uri}/v0/credentials/${gs.gameserver}/k8s`
131
+ url = `${this._stackApiBaseUrl}/v0/credentials/${gs.gameserver}/k8s`
80
132
  }
81
133
  } else if (gs.organization != null && gs.project != null && gs.environment != null) {
82
- url = `${this.stack_api_base_uri}/v1/servers/${gs.organization}/${gs.project}/${gs.environment}/credentials/k8s`
134
+ url = `${this._stackApiBaseUrl}/v1/servers/${gs.organization}/${gs.project}/${gs.environment}/credentials/k8s`
83
135
  } else {
84
136
  throw new Error('Invalid arguments for getKubeConfig')
85
137
  }
@@ -101,6 +153,145 @@ export class StackAPI {
101
153
  return await response.text()
102
154
  }
103
155
 
156
+ /**
157
+ * Get a `kubeconfig` payload which invokes `metaplay-auth get-kubernetes-execcredential` to get the actual
158
+ * access credentials each time the kubeconfig is used.
159
+ * @param gs Game server environment to get credentials for.
160
+ * @returns The kubeconfig YAML.
161
+ */
162
+ async getKubeConfigExecCredential (gs: GameserverId): Promise<string> {
163
+ let url = ''
164
+ let gsSlug = ''
165
+ const execArgs = ['get-kubernetes-execcredential']
166
+ if (gs.gameserver != null) {
167
+ const adminUrl = getGameserverAdminUrl(gs.gameserver)
168
+ url = `https://${adminUrl}/.infra/credentials/k8s?type=execcredential`
169
+ gsSlug = gs.gameserver
170
+ execArgs.push('--gameserver', gs.gameserver)
171
+ } else if (gs.organization != null && gs.project != null && gs.environment != null) {
172
+ url = `${this._stackApiBaseUrl}/v1/servers/${gs.organization}/${gs.project}/${gs.environment}/credentials/k8s?type=execcredential`
173
+ gsSlug = `${gs.organization}.${gs.project}.${gs.environment}`
174
+ execArgs.push('--organization', gs.organization, '--project', gs.project, '--environment', gs.environment)
175
+ } else {
176
+ throw new Error('Invalid arguments for getKubeConfigExecCredential')
177
+ }
178
+
179
+ logger.debug(`Getting Kubernetes KubeConfig from ${url}...`)
180
+
181
+ const response = await fetch(url, {
182
+ method: 'POST',
183
+ headers: {
184
+ Authorization: `Bearer ${this.accessToken}`,
185
+ 'Content-Type': 'application/json'
186
+ }
187
+ })
188
+
189
+ if (response.status !== 200) {
190
+ throw new Error(`Failed to fetch Kubernetes KubeConfig: ${response.statusText}`)
191
+ }
192
+
193
+ // get the execcredential and morph it into a kubeconfig which calls metaplay-auth for the token
194
+ let kubeExecCredential
195
+ try {
196
+ kubeExecCredential = await response.json() as KubeExecCredential
197
+ } catch {
198
+ throw new Error('Failed to fetch Kubernetes KubeConfig: the server response is not JSON')
199
+ }
200
+
201
+ if (!kubeExecCredential.spec.cluster) {
202
+ throw new Error('Received kubeExecCredential with missing spec.cluster')
203
+ }
204
+
205
+ let namespace = 'default'
206
+ let user = 'user'
207
+
208
+ try {
209
+ const environment = await this.getEnvironmentDetails(gs)
210
+ namespace = environment.deployment?.kubernetes_namespace ?? namespace
211
+ } catch (e) {
212
+ logger.debug('Failed to get environment details, using defaults', e)
213
+ }
214
+
215
+ try {
216
+ const userinfo = await getUserinfo(this.accessToken)
217
+ user = userinfo.sub ?? user
218
+ } catch (e) {
219
+ logger.debug('Failed to get userinfo, using defaults', e)
220
+ }
221
+
222
+ const kubeConfig: KubeConfig = {
223
+ apiVersion: 'v1',
224
+ kind: 'Config',
225
+ 'current-context': gsSlug,
226
+ clusters: [
227
+ {
228
+ cluster: {
229
+ 'certificate-authority-data': kubeExecCredential.spec.cluster.certificateAuthorityData,
230
+ server: kubeExecCredential.spec.cluster.server
231
+ },
232
+ name: kubeExecCredential.spec.cluster.server
233
+ }
234
+ ],
235
+ contexts: [
236
+ {
237
+ context: {
238
+ cluster: kubeExecCredential.spec.cluster.server,
239
+ namespace,
240
+ user
241
+ },
242
+ name: gsSlug,
243
+ }
244
+ ],
245
+ users: [
246
+ {
247
+ name: user,
248
+ user: {
249
+ exec: {
250
+ apiVersion: 'client.authentication.k8s.io/v1beta1',
251
+ command: 'metaplay-auth', // todo: figure out how to refer to metaplay-auth itself
252
+ args: execArgs,
253
+ }
254
+ }
255
+ }
256
+ ],
257
+ }
258
+
259
+ // return as yaml for easier consumption
260
+ return dump(kubeConfig)
261
+ }
262
+
263
+ async getKubeExecCredential (gs: GameserverId): Promise<string> {
264
+ let url = ''
265
+ if (gs.gameserver != null) {
266
+ if (isValidFQDN(gs.gameserver)) {
267
+ const adminUrl = getGameserverAdminUrl(gs.gameserver)
268
+ url = `https://${adminUrl}/.infra/credentials/k8s?type=execcredential`
269
+ } else {
270
+ url = `${this._stackApiBaseUrl}/v0/credentials/${gs.gameserver}/k8s?type=execcredential`
271
+ }
272
+ } else if (gs.organization != null && gs.project != null && gs.environment != null) {
273
+ url = `${this._stackApiBaseUrl}/v1/servers/${gs.organization}/${gs.project}/${gs.environment}/credentials/k8s?type=execcredential`
274
+ } else {
275
+ throw new Error('Invalid arguments for getKubeConfig')
276
+ }
277
+
278
+ logger.debug(`Getting Kubernetes ExecCredential from ${url}...`)
279
+
280
+ const response = await fetch(url, {
281
+ method: 'POST',
282
+ headers: {
283
+ Authorization: `Bearer ${this.accessToken}`,
284
+ 'Content-Type': 'application/json'
285
+ }
286
+ })
287
+
288
+ if (response.status !== 200) {
289
+ throw new Error(`Failed to fetch Kubernetes ExecCredential: ${response.statusText}`)
290
+ }
291
+
292
+ return await response.text()
293
+ }
294
+
104
295
  async getEnvironmentDetails (gs: GameserverId): Promise<any> {
105
296
  let url = ''
106
297
  if (gs.gameserver != null) {
@@ -108,10 +299,10 @@ export class StackAPI {
108
299
  const adminUrl = getGameserverAdminUrl(gs.gameserver)
109
300
  url = `https://${adminUrl}/.infra/environment`
110
301
  } else {
111
- url = `${this.stack_api_base_uri}/v0/deployments/${gs.gameserver}`
302
+ url = `${this._stackApiBaseUrl}/v0/deployments/${gs.gameserver}`
112
303
  }
113
304
  } else if (gs.organization != null && gs.project != null && gs.environment != null) {
114
- url = `${this.stack_api_base_uri}/v1/servers/${gs.organization}/${gs.project}/${gs.environment}`
305
+ url = `${this._stackApiBaseUrl}/v1/servers/${gs.organization}/${gs.project}/${gs.environment}`
115
306
  } else {
116
307
  throw new Error('Invalid arguments for environment details')
117
308
  }