@metaplay/metaplay-auth 1.4.2 → 1.5.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/CHANGELOG.md +23 -0
- package/dist/index.cjs +292 -0
- package/dist/sshcrypto-OMBCGRSN.node +0 -0
- package/index.ts +142 -94
- package/package.json +12 -14
- package/src/auth.ts +11 -5
- package/src/deployment.ts +124 -14
- package/src/stackapi.ts +18 -363
- package/src/targetenvironment.ts +307 -0
- package/src/utils.ts +49 -9
- package/src/version.ts +1 -0
- package/dist/index.js +0 -644
- package/dist/index.js.map +0 -1
- package/dist/src/auth.js +0 -373
- package/dist/src/auth.js.map +0 -1
- package/dist/src/deployment.js +0 -250
- package/dist/src/deployment.js.map +0 -1
- package/dist/src/logging.js +0 -18
- package/dist/src/logging.js.map +0 -1
- package/dist/src/secret_store.js +0 -79
- package/dist/src/secret_store.js.map +0 -1
- package/dist/src/stackapi.js +0 -302
- package/dist/src/stackapi.js.map +0 -1
- package/dist/src/utils.js +0 -62
- package/dist/src/utils.js.map +0 -1
- package/dist/tests/utils.spec.js +0 -18
- package/dist/tests/utils.spec.js.map +0 -1
- package/tests/utils.spec.ts +0 -20
- package/vitest.config.ts +0 -7
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
import { logger } from './logging.js'
|
|
2
|
+
import { dump } from 'js-yaml'
|
|
3
|
+
import { getUserinfo } from './auth.js'
|
|
4
|
+
import { isRunningUnderNpx } from './utils.js'
|
|
5
|
+
import { ECRClient, GetAuthorizationTokenCommand } from '@aws-sdk/client-ecr'
|
|
6
|
+
import { defaultStackApiBaseUrl } from './stackapi.js'
|
|
7
|
+
|
|
8
|
+
interface AwsCredentialsResponse {
|
|
9
|
+
AccessKeyId: string
|
|
10
|
+
SecretAccessKey: string
|
|
11
|
+
SessionToken: string
|
|
12
|
+
Expiration: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface DeploymentDetails {
|
|
16
|
+
server_hostname: string
|
|
17
|
+
kubernetes_namespace: string
|
|
18
|
+
ecr_repo: string
|
|
19
|
+
aws_region: string
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface EnvironmentDetails {
|
|
23
|
+
deployment: DeploymentDetails
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface KubeConfig {
|
|
27
|
+
apiVersion?: string
|
|
28
|
+
kind: string
|
|
29
|
+
'current-context': string
|
|
30
|
+
clusters: KubeConfigCluster[]
|
|
31
|
+
contexts: KubeConfigContext[]
|
|
32
|
+
users: KubeConfigUser[]
|
|
33
|
+
preferences?: any
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface KubeConfigCluster {
|
|
37
|
+
cluster: {
|
|
38
|
+
'certificate-authority-data': string
|
|
39
|
+
server: string
|
|
40
|
+
}
|
|
41
|
+
name: string
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface KubeConfigContext {
|
|
45
|
+
context: {
|
|
46
|
+
cluster: string
|
|
47
|
+
user: string
|
|
48
|
+
namespace?: string
|
|
49
|
+
}
|
|
50
|
+
name: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface KubeConfigUser {
|
|
54
|
+
name: string
|
|
55
|
+
user: {
|
|
56
|
+
token?: string
|
|
57
|
+
exec?: {
|
|
58
|
+
command: string
|
|
59
|
+
args: string[]
|
|
60
|
+
apiVersion: string
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface KubeExecCredential {
|
|
66
|
+
apiVersion: string
|
|
67
|
+
kind: string
|
|
68
|
+
spec: {
|
|
69
|
+
cluster?: {
|
|
70
|
+
server: string
|
|
71
|
+
certificateAuthorityData: string
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
status: {
|
|
75
|
+
token: string
|
|
76
|
+
expirationTimestamp: string
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface DockerCredentials {
|
|
81
|
+
username: string
|
|
82
|
+
password: string
|
|
83
|
+
registryUrl: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export class TargetEnvironment {
|
|
87
|
+
private readonly accessToken: string
|
|
88
|
+
private readonly environmentDomain: string
|
|
89
|
+
private readonly stackApiBaseUrl: string
|
|
90
|
+
private readonly environmentApiBaseUrl: string
|
|
91
|
+
private readonly organization?: string
|
|
92
|
+
private readonly project?: string
|
|
93
|
+
private readonly environment?: string
|
|
94
|
+
|
|
95
|
+
constructor (accessToken: string, environmentDomain: string, stackApiBaseUrl: string, environmentApiBaseUrl: string, organization?: string, project?: string, environment?: string) {
|
|
96
|
+
this.accessToken = accessToken
|
|
97
|
+
this.environmentDomain = environmentDomain
|
|
98
|
+
this.stackApiBaseUrl = stackApiBaseUrl
|
|
99
|
+
this.environmentApiBaseUrl = environmentApiBaseUrl
|
|
100
|
+
this.organization = organization
|
|
101
|
+
this.project = project
|
|
102
|
+
this.environment = environment
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
async fetchJson<T> (url: string, method: string): Promise<T> {
|
|
106
|
+
const response = await fetch(url, {
|
|
107
|
+
method,
|
|
108
|
+
headers: {
|
|
109
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
110
|
+
'Content-Type': 'application/json'
|
|
111
|
+
}
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
if (response.status !== 200) {
|
|
115
|
+
throw new Error(`Failed to fetch ${method} ${url}: ${response.statusText}, response code=${response.status}`)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return await response.json() as T
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async fetchText (url: string, method: string): Promise<string> {
|
|
122
|
+
let response
|
|
123
|
+
try {
|
|
124
|
+
response = await fetch(url, {
|
|
125
|
+
method,
|
|
126
|
+
headers: {
|
|
127
|
+
Authorization: `Bearer ${this.accessToken}`,
|
|
128
|
+
'Content-Type': 'application/json'
|
|
129
|
+
}
|
|
130
|
+
})
|
|
131
|
+
} catch (error: any) {
|
|
132
|
+
logger.error(`Failed to fetch ${method} ${url}:`, error)
|
|
133
|
+
if (error.cause?.code === 'UNABLE_TO_VERIFY_LEAF_SIGNATURE') {
|
|
134
|
+
throw new Error(`Failed to to fetch ${url}: SSL certificate validation failed. Is someone trying to tamper with your internet connection?`)
|
|
135
|
+
}
|
|
136
|
+
throw new Error(`Failed to fetch ${url}: ${error}`)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (response.status !== 200) {
|
|
140
|
+
throw new Error(`Failed to fetch ${method} ${url}: ${response.statusText}`)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return await response.text()
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
async getAwsCredentials (): Promise<AwsCredentialsResponse> {
|
|
147
|
+
const url = `${this.environmentApiBaseUrl}/credentials/aws`
|
|
148
|
+
logger.debug(`Fetching AWS credentials from ${url}...`)
|
|
149
|
+
return await this.fetchJson<AwsCredentialsResponse>(url, 'POST')
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async getKubeConfig (): Promise<string> {
|
|
153
|
+
const url = `${this.environmentApiBaseUrl}/credentials/k8s`
|
|
154
|
+
logger.debug(`Getting KubeConfig from ${url}...`)
|
|
155
|
+
return await this.fetchText(url, 'POST')
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Get a `kubeconfig` payload which invokes `metaplay-auth get-kubernetes-execcredential` to get the actual
|
|
160
|
+
* access credentials each time the kubeconfig is used.
|
|
161
|
+
* @returns The kubeconfig YAML.
|
|
162
|
+
*/
|
|
163
|
+
async getKubeConfigExecCredential (): Promise<string> {
|
|
164
|
+
const url = `${this.environmentApiBaseUrl}/credentials/k8s?type=execcredential`
|
|
165
|
+
logger.debug(`Getting Kubernetes KubeConfig from ${url}...`)
|
|
166
|
+
|
|
167
|
+
// get the execcredential and morph it into a kubeconfig which calls metaplay-auth for the token
|
|
168
|
+
let kubeExecCredential: KubeExecCredential
|
|
169
|
+
try {
|
|
170
|
+
kubeExecCredential = await this.fetchJson<KubeExecCredential>(url, 'POST')
|
|
171
|
+
} catch {
|
|
172
|
+
throw new Error('Failed to fetch Kubernetes KubeConfig')
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (!kubeExecCredential.spec.cluster) {
|
|
176
|
+
throw new Error('Received kubeExecCredential with missing spec.cluster')
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Fetch environment namespace
|
|
180
|
+
const environment = await this.getEnvironmentDetails()
|
|
181
|
+
const namespace = environment.deployment?.kubernetes_namespace
|
|
182
|
+
if (!namespace) {
|
|
183
|
+
throw new Error('Environment details did not contain a valid Kubernetes namespace')
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Fetch user id
|
|
187
|
+
const userinfo = await getUserinfo(this.accessToken)
|
|
188
|
+
const userId = userinfo.email
|
|
189
|
+
|
|
190
|
+
// Resolve the environment identity (prefer org-proj-env or fall back to FQDN)
|
|
191
|
+
const envId = this.organization ? `${this.organization}-${this.project}-${this.environment}` : this.environmentDomain
|
|
192
|
+
|
|
193
|
+
// Resolve invocation for getting the kubeconfig exec credential
|
|
194
|
+
let execCmd = 'metaplay-auth'
|
|
195
|
+
let execArgs =
|
|
196
|
+
['get-kubernetes-execcredential']
|
|
197
|
+
.concat(this.stackApiBaseUrl !== defaultStackApiBaseUrl ? ['--stack-api', this.stackApiBaseUrl] : [])
|
|
198
|
+
.concat([envId])
|
|
199
|
+
|
|
200
|
+
// If running under npx, use npx also for getting the kube exec credential
|
|
201
|
+
if (isRunningUnderNpx()) {
|
|
202
|
+
execCmd = 'npx'
|
|
203
|
+
execArgs = [
|
|
204
|
+
'--yes', // For NPX to silently install or update
|
|
205
|
+
'@metaplay/metaplay-auth@latest',
|
|
206
|
+
...execArgs
|
|
207
|
+
]
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const kubeConfig: KubeConfig = {
|
|
211
|
+
apiVersion: 'v1',
|
|
212
|
+
kind: 'Config',
|
|
213
|
+
'current-context': envId,
|
|
214
|
+
clusters: [
|
|
215
|
+
{
|
|
216
|
+
cluster: {
|
|
217
|
+
'certificate-authority-data': kubeExecCredential.spec.cluster.certificateAuthorityData,
|
|
218
|
+
server: kubeExecCredential.spec.cluster.server
|
|
219
|
+
},
|
|
220
|
+
name: kubeExecCredential.spec.cluster.server
|
|
221
|
+
}
|
|
222
|
+
],
|
|
223
|
+
contexts: [
|
|
224
|
+
{
|
|
225
|
+
context: {
|
|
226
|
+
cluster: kubeExecCredential.spec.cluster.server,
|
|
227
|
+
namespace,
|
|
228
|
+
user: userId
|
|
229
|
+
},
|
|
230
|
+
name: envId,
|
|
231
|
+
}
|
|
232
|
+
],
|
|
233
|
+
users: [
|
|
234
|
+
{
|
|
235
|
+
name: userId,
|
|
236
|
+
user: {
|
|
237
|
+
exec: {
|
|
238
|
+
apiVersion: 'client.authentication.k8s.io/v1beta1',
|
|
239
|
+
command: execCmd,
|
|
240
|
+
args: execArgs
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
],
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// return as yaml for easier consumption
|
|
248
|
+
return dump(kubeConfig)
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async getKubeExecCredential (): Promise<string> {
|
|
252
|
+
const url = `${this.environmentApiBaseUrl}/credentials/k8s?type=execcredential`
|
|
253
|
+
logger.debug(`Getting Kubernetes ExecCredential from ${url}...`)
|
|
254
|
+
return await this.fetchText(url, 'POST')
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async getEnvironmentDetails (): Promise<EnvironmentDetails> {
|
|
258
|
+
// \note If invoking StackAPI via the https://<environment>/.infra, we need to add '/environment' to the path
|
|
259
|
+
const urlSuffix = this.environmentApiBaseUrl.endsWith('.infra') ? '/environment' : ''
|
|
260
|
+
const url = this.environmentApiBaseUrl + urlSuffix
|
|
261
|
+
logger.debug(`Getting environment details from ${url}...`)
|
|
262
|
+
return await this.fetchJson<EnvironmentDetails>(url, 'GET')
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
async getDockerCredentials (): Promise<DockerCredentials> {
|
|
266
|
+
// Get environment info (for AWS region)
|
|
267
|
+
logger.debug('Get environment info')
|
|
268
|
+
const envInfo = await this.getEnvironmentDetails()
|
|
269
|
+
|
|
270
|
+
// Fetch AWS credentials from Metaplay cloud
|
|
271
|
+
logger.debug('Get AWS credentials from Metaplay')
|
|
272
|
+
const awsCredentials = await this.getAwsCredentials()
|
|
273
|
+
|
|
274
|
+
// Create ECR client with credentials
|
|
275
|
+
logger.debug('Create ECR client')
|
|
276
|
+
const client = new ECRClient({
|
|
277
|
+
credentials: {
|
|
278
|
+
accessKeyId: awsCredentials.AccessKeyId,
|
|
279
|
+
secretAccessKey: awsCredentials.SecretAccessKey,
|
|
280
|
+
sessionToken: awsCredentials.SessionToken
|
|
281
|
+
},
|
|
282
|
+
region: envInfo.deployment.aws_region
|
|
283
|
+
})
|
|
284
|
+
|
|
285
|
+
// Fetch the ECR docker authentication token
|
|
286
|
+
logger.debug('Fetch ECR login credentials from AWS')
|
|
287
|
+
const command = new GetAuthorizationTokenCommand({})
|
|
288
|
+
const response = await client.send(command)
|
|
289
|
+
if (!response.authorizationData || response.authorizationData.length === 0 || !response.authorizationData[0].authorizationToken || !response.authorizationData[0].proxyEndpoint) {
|
|
290
|
+
throw new Error('Received an empty authorization token response for ECR repository')
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Parse username and password from the response (separated by a ':')
|
|
294
|
+
logger.debug('Parse ECR response')
|
|
295
|
+
const registryUrl = response.authorizationData[0].proxyEndpoint
|
|
296
|
+
const authorization64 = response.authorizationData[0].authorizationToken
|
|
297
|
+
const authorization = Buffer.from(authorization64, 'base64').toString()
|
|
298
|
+
const [username, password] = authorization.split(':')
|
|
299
|
+
logger.debug(`ECR: username=${username}, proxyEndpoint=${registryUrl}`)
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
username,
|
|
303
|
+
password,
|
|
304
|
+
registryUrl
|
|
305
|
+
} satisfies DockerCredentials
|
|
306
|
+
}
|
|
307
|
+
}
|
package/src/utils.ts
CHANGED
|
@@ -1,8 +1,5 @@
|
|
|
1
1
|
import { spawn } from 'child_process'
|
|
2
|
-
|
|
3
|
-
export function stackApiBaseFromOptions (arg: string, options: Record<string, any>): string {
|
|
4
|
-
return ''
|
|
5
|
-
}
|
|
2
|
+
import path from 'path'
|
|
6
3
|
|
|
7
4
|
/**
|
|
8
5
|
* Checks if the given string is a fully qualified domain name (FQDN).
|
|
@@ -49,15 +46,27 @@ export function getGameserverAdminUrl (uri: string): string {
|
|
|
49
46
|
return `${hostname}-admin.${domain}`
|
|
50
47
|
}
|
|
51
48
|
|
|
52
|
-
interface
|
|
53
|
-
|
|
49
|
+
export interface ExecuteCommandResult {
|
|
50
|
+
exitCode: number | null
|
|
54
51
|
stdout: any[]
|
|
55
52
|
stderr: any[]
|
|
56
53
|
}
|
|
57
54
|
|
|
58
|
-
|
|
55
|
+
/**
|
|
56
|
+
* Run a child process and return an awaitable Promise.
|
|
57
|
+
*
|
|
58
|
+
* @param command Name of the command/binary to execute.
|
|
59
|
+
* @param args List of arugments to pass to the child process.
|
|
60
|
+
* @param inheritStdio Should the stdin/stdout/stderr be inherited from the parent process? If false, the outputs are buffered for reading later.
|
|
61
|
+
* @param env Environment variables to pass in. Does not inherit parent process env. Use current process env if not specified.
|
|
62
|
+
* @returns Awaitable promise with the result of the execution.
|
|
63
|
+
*/
|
|
64
|
+
export async function executeCommand (command: string, args: string[], inheritStdio: boolean, env?: NodeJS.ProcessEnv): Promise<ExecuteCommandResult> {
|
|
59
65
|
return await new Promise((resolve, reject) => {
|
|
60
|
-
const childProcess = spawn(command, args
|
|
66
|
+
const childProcess = spawn(command, args, {
|
|
67
|
+
stdio: inheritStdio ? 'inherit' : undefined,
|
|
68
|
+
env,
|
|
69
|
+
})
|
|
61
70
|
let stdout: any[] = []
|
|
62
71
|
let stderr: any[] = []
|
|
63
72
|
|
|
@@ -71,7 +80,7 @@ export async function executeCommand (command: string, args: string[]): Promise<
|
|
|
71
80
|
|
|
72
81
|
// If program runs to completion, resolve promise with success
|
|
73
82
|
childProcess.on('close', (code) => {
|
|
74
|
-
resolve({ code, stdout, stderr })
|
|
83
|
+
resolve({ exitCode: code, stdout, stderr })
|
|
75
84
|
})
|
|
76
85
|
|
|
77
86
|
childProcess.on('error', (err) => {
|
|
@@ -83,3 +92,34 @@ export async function executeCommand (command: string, args: string[]): Promise<
|
|
|
83
92
|
})
|
|
84
93
|
})
|
|
85
94
|
}
|
|
95
|
+
|
|
96
|
+
export function isRunningUnderNpx () {
|
|
97
|
+
// console.log('process.argv:', process.argv)
|
|
98
|
+
const matchBinary = (binaryPath: string, binaryName: string): boolean => {
|
|
99
|
+
const binary = path.basename(binaryPath, path.extname(binaryPath)) // drop path and extension
|
|
100
|
+
return binary === binaryName
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Check if 'npx' is the invoked binary (this check is not sufficient as it can also be 'node')
|
|
104
|
+
if (matchBinary(process.argv[0], 'npx')) {
|
|
105
|
+
return true
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Check if the script path contains a known npx temporary directory pattern
|
|
109
|
+
const scriptPath = process.argv[1]
|
|
110
|
+
if (scriptPath?.includes(`${path.sep}_npx${path.sep}`)) {
|
|
111
|
+
return true
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Check if the environment variable 'npm_execpath' is exactly 'npx'
|
|
115
|
+
if (process.env.npm_execpath && matchBinary(process.env.npm_execpath, 'npx')) {
|
|
116
|
+
return true
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Check if the environment variable '_' ends with 'npx' (specific to npx usage)
|
|
120
|
+
if (process.env._ && matchBinary(process.env._, 'npx')) {
|
|
121
|
+
return true
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return false
|
|
125
|
+
}
|
package/src/version.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const PACKAGE_VERSION = '1.5.0'
|