Skip to content

Commit cababe3

Browse files
authored
Add PAT scope foundation: request-context plugin, permission tagging, migrations, and models (#7580)
1 parent 4f57168 commit cababe3

10 files changed

Lines changed: 661 additions & 168 deletions
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
/**
2+
* Add readOnly and adminOptIn columns to AccessTokens
3+
*/
4+
5+
const { DataTypes } = require('sequelize')
6+
7+
module.exports = {
8+
/**
9+
* upgrade database
10+
* @param {QueryInterface} context Sequelize.QueryInterface
11+
*/
12+
up: async (context, Sequelize) => {
13+
await context.addColumn('AccessTokens', 'readOnly', {
14+
type: DataTypes.BOOLEAN,
15+
defaultValue: false,
16+
allowNull: false
17+
})
18+
await context.addColumn('AccessTokens', 'adminOptIn', {
19+
type: DataTypes.BOOLEAN,
20+
defaultValue: false,
21+
allowNull: false
22+
})
23+
},
24+
down: async (context) => {}
25+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**
2+
* Create AccessTokenTeamScopes join table for PAT team scoping
3+
*/
4+
5+
const { DataTypes } = require('sequelize')
6+
7+
module.exports = {
8+
/**
9+
* upgrade database
10+
* @param {QueryInterface} context Sequelize.QueryInterface
11+
*/
12+
up: async (context, Sequelize) => {
13+
await context.createTable('AccessTokenTeamScopes', {
14+
id: {
15+
type: DataTypes.INTEGER,
16+
primaryKey: true,
17+
autoIncrement: true
18+
},
19+
AccessTokenId: {
20+
type: DataTypes.INTEGER,
21+
references: { model: 'AccessTokens', key: 'id' },
22+
onDelete: 'CASCADE',
23+
onUpdate: 'CASCADE',
24+
allowNull: false
25+
},
26+
TeamId: {
27+
type: DataTypes.INTEGER,
28+
references: { model: 'Teams', key: 'id' },
29+
onDelete: 'CASCADE',
30+
onUpdate: 'CASCADE',
31+
allowNull: false
32+
},
33+
UserId: {
34+
type: DataTypes.INTEGER,
35+
references: { model: 'Users', key: 'id' },
36+
onDelete: 'CASCADE',
37+
onUpdate: 'CASCADE',
38+
allowNull: false
39+
},
40+
createdAt: {
41+
type: DataTypes.DATE,
42+
allowNull: false
43+
},
44+
updatedAt: {
45+
type: DataTypes.DATE,
46+
allowNull: false
47+
}
48+
})
49+
50+
await context.addIndex('AccessTokenTeamScopes', {
51+
name: 'access_token_team_scope_unique',
52+
fields: ['AccessTokenId', 'TeamId'],
53+
unique: true
54+
})
55+
await context.addIndex('AccessTokenTeamScopes', {
56+
name: 'access_token_team_scope_user_team',
57+
fields: ['UserId', 'TeamId']
58+
})
59+
},
60+
down: async (context) => {}
61+
}

forge/db/models/AccessToken.js

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,13 +45,16 @@ module.exports = {
4545
}
4646
}
4747
},
48-
name: { type: DataTypes.STRING }
48+
name: { type: DataTypes.STRING },
49+
readOnly: { type: DataTypes.BOOLEAN, defaultValue: false, allowNull: false },
50+
adminOptIn: { type: DataTypes.BOOLEAN, defaultValue: false, allowNull: false }
4951
},
5052
associations: function (M) {
5153
this.belongsTo(M.Team, { foreignKey: 'ownerId', constraints: false })
5254
this.belongsTo(M.Project, { foreignKey: 'ownerId', constraints: false })
5355
this.belongsTo(M.Device, { foreignKey: 'ownerId', constraints: false })
5456
this.belongsTo(M.User, { foreignKey: 'ownerId', constraints: false })
57+
this.hasMany(M.AccessTokenTeamScope)
5558
},
5659
finders: function (M) {
5760
return {
@@ -112,7 +115,14 @@ module.exports = {
112115
name: { [Op.ne]: null }
113116
},
114117
order: [['id', 'ASC']],
115-
attributes: ['id', 'name', 'scope', 'expiresAt']
118+
attributes: ['id', 'name', 'scope', 'expiresAt', 'readOnly', 'adminOptIn'],
119+
include: [{
120+
model: M.AccessTokenTeamScope,
121+
include: [{
122+
model: M.Team,
123+
attributes: ['id', 'name']
124+
}]
125+
}]
116126
})
117127
return tokens
118128
},
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
/**
2+
* AccessTokenTeamScope join table
3+
* Links scoped PATs to the teams they are allowed to access.
4+
*/
5+
const { DataTypes } = require('sequelize')
6+
7+
module.exports = {
8+
name: 'AccessTokenTeamScope',
9+
schema: {
10+
id: {
11+
type: DataTypes.INTEGER,
12+
primaryKey: true,
13+
autoIncrement: true
14+
}
15+
},
16+
meta: {
17+
slug: false,
18+
hashid: false,
19+
links: false
20+
},
21+
indexes: [
22+
{ name: 'access_token_team_scope_unique', fields: ['AccessTokenId', 'TeamId'], unique: true },
23+
{ name: 'access_token_team_scope_user_team', fields: ['UserId', 'TeamId'] }
24+
],
25+
associations: function (M) {
26+
this.belongsTo(M.AccessToken)
27+
this.belongsTo(M.Team)
28+
this.belongsTo(M.User)
29+
}
30+
}

forge/db/models/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ const modelTypes = [
6767
'ProjectTemplate',
6868
'ProjectSnapshot',
6969
'AccessToken',
70+
'AccessTokenTeamScope',
7071
'AuthClient',
7172
'Device',
7273
'DeviceGroup',

forge/forge.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
const cookie = require('@fastify/cookie')
22
const csrf = require('@fastify/csrf-protection')
33
const helmet = require('@fastify/helmet')
4+
const { fastifyRequestContext } = require('@fastify/request-context')
45
const Sentry = require('@sentry/node')
56
const fastify = require('fastify')
67

@@ -212,6 +213,9 @@ module.exports = async (options = {}) => {
212213
})
213214
await server.register(csrf, { cookieOpts: { _signed: true, _httpOnly: true } })
214215

216+
// Request Context: per-request store
217+
await server.register(fastifyRequestContext)
218+
215219
let contentSecurityPolicy = false
216220
if (runtimeConfig.content_security_policy?.enabled) {
217221
if (!runtimeConfig.content_security_policy.directives) {

0 commit comments

Comments
 (0)