-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcert-manager.js
More file actions
372 lines (326 loc) · 10.4 KB
/
cert-manager.js
File metadata and controls
372 lines (326 loc) · 10.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
const { getOctokit } = require('@actions/github')
/**
* 使用 GraphQL 查询用户的证书和吊销记录
* @param {string} token - GitHub token
* @param {string} owner - 仓库所有者
* @param {string} repo - 仓库名
* @param {string} username - 用户名
* @returns {Promise<{keyringIssues: Array, revokeIssues: Array}>}
*/
async function fetchUserCertificateIssues (token, owner, repo, username) {
const octokit = getOctokit(token)
const query = `
query($owner: String!, $repo: String!, $username: String!) {
repository(owner: $owner, name: $repo) {
keyringIssues: issues(
first: 100
filterBy: { createdBy: $username, states: CLOSED, labels: ["approved"] }
orderBy: { field: CREATED_AT, direction: DESC }
) {
nodes {
number
title
createdAt
closedAt
body
labels(first: 10) {
nodes {
name
}
}
comments(first: 100) {
nodes {
id
body
createdAt
author {
login
}
}
}
}
}
revokeIssues: issues(
first: 100
filterBy: { createdBy: $username, states: CLOSED, labels: ["revoked"] }
orderBy: { field: CREATED_AT, direction: DESC }
) {
nodes {
number
title
body
createdAt
closedAt
}
}
}
}
`
try {
const result = await octokit.graphql(query, {
owner,
repo,
username
})
return {
keyringIssues: result.repository.keyringIssues.nodes || [],
revokeIssues: result.repository.revokeIssues.nodes || []
}
} catch (error) {
console.error('GraphQL query failed:', error)
throw new Error(`Failed to fetch certificate issues: ${error.message}`)
}
}
/**
* 从 issue 评论中提取证书信息
* @param {Array} comments - Issue 评论列表
* @returns {{serialNumber: string, fingerprint: string, issuedAt: Date, expiresAt: Date} | null}
*/
function extractCertificateInfo (comments) {
// 查找证书签发评论
const certComment = comments.find(c =>
c.body &&
c.body.includes('✅ Certificate successfully issued') &&
c.body.includes('Serial Number')
)
if (!certComment) {
return null
}
// 提取序列号
const serialMatch = certComment.body.match(/Serial Number.*?`([^`]+)`/i)
const serialNumber = serialMatch ? serialMatch[1] : null
// 提取指纹
const fingerprintMatch = certComment.body.match(/Fingerprint \(SHA-256\).*?`([^`]+)`/i)
const fingerprint = fingerprintMatch ? fingerprintMatch[1] : null
if (!serialNumber) {
return null
}
// 计算有效期(1年)
const issuedAt = new Date(certComment.createdAt)
const expiresAt = new Date(issuedAt.getTime() + 365 * 24 * 60 * 60 * 1000)
return {
serialNumber,
fingerprint,
issuedAt,
expiresAt
}
}
/**
* 检查证书是否在吊销列表中
* @param {string} serialNumber - 证书序列号
* @param {Array} revokeIssues - 吊销 issue 列表
* @returns {boolean}
*/
function isCertificateRevoked (serialNumber, revokeIssues) {
return revokeIssues.some(issue =>
issue.title.toLowerCase().includes('[revoke]') &&
issue.body &&
issue.body.includes(serialNumber)
)
}
/**
* 检查用户是否已有有效证书
* @param {string} username - GitHub 用户名
* @param {string} token - GitHub token
* @param {string} owner - 仓库所有者
* @param {string} repo - 仓库名
* @returns {Promise<{hasActiveCert: boolean, certificate?: object, issueNumber?: number}>}
*/
async function checkExistingCertificate (username, token, owner, repo) {
try {
console.log(`Checking existing certificates for user: ${username}`)
// 使用 GraphQL 获取用户的 issue
const { keyringIssues, revokeIssues } = await fetchUserCertificateIssues(
token,
owner,
repo,
username
)
console.log(`Found ${keyringIssues.length} keyring issues and ${revokeIssues.length} revoke issues`)
// 过滤出真正的 keyring issue(标题包含 [keyring])
const validKeyringIssues = keyringIssues.filter(issue =>
issue.title.toLowerCase().includes('[keyring]')
)
if (validKeyringIssues.length === 0) {
console.log('No keyring issues found for user')
return { hasActiveCert: false }
}
const now = new Date()
// 检查每个 keyring issue,从最新到最旧
for (const issue of validKeyringIssues) {
const certInfo = extractCertificateInfo(issue.comments.nodes)
if (!certInfo) {
console.log(`Issue #${issue.number}: No certificate info found`)
continue
}
console.log(`Issue #${issue.number}: Found certificate ${certInfo.serialNumber}`)
console.log(` Issued: ${certInfo.issuedAt.toISOString()}`)
console.log(` Expires: ${certInfo.expiresAt.toISOString()}`)
// 检查是否过期
if (now > certInfo.expiresAt) {
console.log(` Status: EXPIRED`)
continue
}
// 检查是否被吊销
const isRevoked = isCertificateRevoked(certInfo.serialNumber, revokeIssues)
if (isRevoked) {
console.log(` Status: REVOKED`)
continue
}
// 找到有效证书
console.log(` Status: ACTIVE`)
return {
hasActiveCert: true,
certificate: {
serialNumber: certInfo.serialNumber,
fingerprint: certInfo.fingerprint,
issuedAt: certInfo.issuedAt.toISOString(),
expiresAt: certInfo.expiresAt.toISOString()
},
issueNumber: issue.number
}
}
console.log('No active certificate found')
return { hasActiveCert: false }
} catch (error) {
console.error('Error checking existing certificate:', error)
// 如果检查失败,为了安全起见,假设没有证书(允许签发)
// 但记录错误以便调试
console.error('Certificate check failed, allowing issuance by default')
return { hasActiveCert: false, error: error.message }
}
}
/**
* 统计用户的证书情况
* @param {string} username - GitHub 用户名
* @param {string} token - GitHub token
* @param {string} owner - 仓库所有者
* @param {string} repo - 仓库名
* @returns {Promise<{total: number, active: number, expired: number, revoked: number}>}
*/
async function getCertificateStatistics (username, token, owner, repo) {
const { keyringIssues, revokeIssues } = await fetchUserCertificateIssues(
token,
owner,
repo,
username
)
const validKeyringIssues = keyringIssues.filter(issue =>
issue.title.toLowerCase().includes('[keyring]')
)
let total = 0
let active = 0
let expired = 0
let revoked = 0
const now = new Date()
for (const issue of validKeyringIssues) {
const certInfo = extractCertificateInfo(issue.comments.nodes)
if (!certInfo) continue
total++
const isRevoked = isCertificateRevoked(certInfo.serialNumber, revokeIssues)
if (isRevoked) {
revoked++
} else if (now > certInfo.expiresAt) {
expired++
} else {
active++
}
}
return { total, active, expired, revoked }
}
/**
* 验证证书是否属于指定用户
* @param {string} serialNumber - 证书序列号
* @param {string} username - 声称拥有证书的用户名
* @param {string} token - GitHub token
* @param {string} owner - 仓库所有者
* @param {string} repo - 仓库名
* @returns {Promise<{isOwner: boolean, actualOwner?: string, issueNumber?: number}>}
*/
async function verifyCertificateOwnership (serialNumber, username, token, owner, repo) {
try {
console.log(`Verifying certificate ownership: serial=${serialNumber}, claimed user=${username}`)
const octokit = require('@actions/github').getOctokit(token)
// GraphQL 查询所有带有 approved 标签的已关闭 keyring issues
const query = `
query($owner: String!, $repo: String!) {
repository(owner: $owner, name: $repo) {
issues(
first: 100
filterBy: { states: CLOSED, labels: ["approved"] }
orderBy: { field: CREATED_AT, direction: DESC }
) {
nodes {
number
title
author {
login
}
comments(first: 100) {
nodes {
body
createdAt
}
}
}
}
}
}
`
const result = await octokit.graphql(query, { owner, repo })
const issues = result.repository.issues.nodes
// 搜索包含该序列号的 issue
for (const issue of issues) {
if (!issue.title.toLowerCase().includes('[keyring]')) continue
const certComment = issue.comments.nodes.find(c =>
c.body &&
c.body.includes('✅ Certificate successfully issued') &&
c.body.includes(serialNumber)
)
if (certComment) {
const actualOwner = issue.author.login
console.log(`Found certificate: serial=${serialNumber}, owner=${actualOwner}, issue=#${issue.number}`)
return {
isOwner: actualOwner.toLowerCase() === username.toLowerCase(),
actualOwner,
issueNumber: issue.number
}
}
}
console.log(`Certificate not found: serial=${serialNumber}`)
return { isOwner: false }
} catch (error) {
console.error('Error verifying certificate ownership:', error)
throw new Error(`Failed to verify certificate ownership: ${error.message}`)
}
}
/**
* 检查用户是否是组织管理员
* @param {string} username - 用户名
* @param {string} token - GitHub token
* @param {string} owner - 组织名称
* @returns {Promise<boolean>}
*/
async function isOrgAdmin (username, token, owner) {
try {
const octokit = require('@actions/github').getOctokit(token)
const { data } = await octokit.rest.orgs.getMembershipForUser({
org: owner,
username
})
// role 可以是 'admin' 或 'member'
return data.role === 'admin'
} catch (error) {
console.error(`Error checking org admin status for ${username}:`, error.message)
return false
}
}
module.exports = {
checkExistingCertificate,
fetchUserCertificateIssues,
getCertificateStatistics,
extractCertificateInfo,
isCertificateRevoked,
verifyCertificateOwnership,
isOrgAdmin
}