Skip to content
25 changes: 25 additions & 0 deletions forge/db/migrations/20260624-01-add-pat-scope-fields.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Add readOnly and adminOptIn columns to AccessTokens
*/

const { DataTypes } = require('sequelize')

module.exports = {
/**
* upgrade database
* @param {QueryInterface} context Sequelize.QueryInterface
*/
up: async (context, Sequelize) => {
await context.addColumn('AccessTokens', 'readOnly', {
type: DataTypes.BOOLEAN,
defaultValue: false,
allowNull: false
})
await context.addColumn('AccessTokens', 'adminOptIn', {
type: DataTypes.BOOLEAN,
defaultValue: false,
allowNull: false
})
},
down: async (context) => {}
}
61 changes: 61 additions & 0 deletions forge/db/migrations/20260624-02-add-access-token-team-scope.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**
* Create AccessTokenTeamScopes join table for PAT team scoping
*/

const { DataTypes } = require('sequelize')

module.exports = {
/**
* upgrade database
* @param {QueryInterface} context Sequelize.QueryInterface
*/
up: async (context, Sequelize) => {
await context.createTable('AccessTokenTeamScopes', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
},
AccessTokenId: {
type: DataTypes.INTEGER,
references: { model: 'AccessTokens', key: 'id' },
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
allowNull: false
},
TeamId: {
type: DataTypes.INTEGER,
references: { model: 'Teams', key: 'id' },
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
allowNull: false
},
UserId: {
type: DataTypes.INTEGER,
references: { model: 'Users', key: 'id' },
onDelete: 'CASCADE',
onUpdate: 'CASCADE',
allowNull: false
},
createdAt: {
type: DataTypes.DATE,
allowNull: false
},
updatedAt: {
type: DataTypes.DATE,
allowNull: false
}
})

await context.addIndex('AccessTokenTeamScopes', {
name: 'access_token_team_scope_unique',
fields: ['AccessTokenId', 'TeamId'],
unique: true
})
await context.addIndex('AccessTokenTeamScopes', {
name: 'access_token_team_scope_user_team',
fields: ['UserId', 'TeamId']
})
},
down: async (context) => {}
}
14 changes: 12 additions & 2 deletions forge/db/models/AccessToken.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,16 @@ module.exports = {
}
}
},
name: { type: DataTypes.STRING }
name: { type: DataTypes.STRING },
readOnly: { type: DataTypes.BOOLEAN, defaultValue: false, allowNull: false },
adminOptIn: { type: DataTypes.BOOLEAN, defaultValue: false, allowNull: false }
},
associations: function (M) {
this.belongsTo(M.Team, { foreignKey: 'ownerId', constraints: false })
this.belongsTo(M.Project, { foreignKey: 'ownerId', constraints: false })
this.belongsTo(M.Device, { foreignKey: 'ownerId', constraints: false })
this.belongsTo(M.User, { foreignKey: 'ownerId', constraints: false })
this.hasMany(M.AccessTokenTeamScope)
},
finders: function (M) {
return {
Expand Down Expand Up @@ -112,7 +115,14 @@ module.exports = {
name: { [Op.ne]: null }
},
order: [['id', 'ASC']],
attributes: ['id', 'name', 'scope', 'expiresAt']
attributes: ['id', 'name', 'scope', 'expiresAt', 'readOnly', 'adminOptIn'],
include: [{
model: M.AccessTokenTeamScope,
include: [{
model: M.Team,
attributes: ['id', 'name']
}]
}]
})
return tokens
},
Expand Down
30 changes: 30 additions & 0 deletions forge/db/models/AccessTokenTeamScope.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* AccessTokenTeamScope join table
* Links scoped PATs to the teams they are allowed to access.
*/
const { DataTypes } = require('sequelize')

module.exports = {
name: 'AccessTokenTeamScope',
schema: {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true
}
},
meta: {
slug: false,
hashid: false,
links: false
},
indexes: [
{ name: 'access_token_team_scope_unique', fields: ['AccessTokenId', 'TeamId'], unique: true },
{ name: 'access_token_team_scope_user_team', fields: ['UserId', 'TeamId'] }
],
associations: function (M) {
this.belongsTo(M.AccessToken)
this.belongsTo(M.Team)
this.belongsTo(M.User)
}
}
1 change: 1 addition & 0 deletions forge/db/models/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ const modelTypes = [
'ProjectTemplate',
'ProjectSnapshot',
'AccessToken',
'AccessTokenTeamScope',
'AuthClient',
'Device',
'DeviceGroup',
Expand Down
4 changes: 4 additions & 0 deletions forge/forge.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const cookie = require('@fastify/cookie')
const csrf = require('@fastify/csrf-protection')
const helmet = require('@fastify/helmet')
const { fastifyRequestContext } = require('@fastify/request-context')
const Sentry = require('@sentry/node')
const fastify = require('fastify')

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

// Request Context: per-request store
await server.register(fastifyRequestContext)

let contentSecurityPolicy = false
if (runtimeConfig.content_security_policy?.enabled) {
if (!runtimeConfig.content_security_policy.directives) {
Expand Down
Loading
Loading