Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Token api tidy up #3717

Merged
merged 6 commits into from
Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 20 additions & 62 deletions forge/db/controllers/AccessToken.js
Original file line number Diff line number Diff line change
Expand Up @@ -209,52 +209,28 @@ module.exports = {
ownerId: '' + userId,
ownerType: 'user'
})
return {
id: tok.id,
name,
token,
expiresAt
}
// Overwrite the hashed token with the plain value
const result = app.db.views.AccessToken.personalAccessTokenSummary(tok)
result.token = token
return result
},
updatePersonalAccessToken: async function (app, user, tokenId, scope, expiresAt) {
const userId = typeof user === 'number' ? user : user.id
const token = await app.db.models.AccessToken.byId(tokenId)
const token = await app.db.models.AccessToken.byId(tokenId, 'user', userId)
if (token) {
if (token.ownerType === 'user' && token.ownerId === '' + userId) {
token.scope = scope
if (expiresAt === undefined) {
token.expiresAt = null
} else {
token.expiresAt = expiresAt
}
await token.save()
token.scope = scope
if (expiresAt === undefined) {
token.expiresAt = null
} else {
// should throw error
throw new Error('Not Authorized')
token.expiresAt = expiresAt
}
await token.save()
} else {
// should throw unknown token error
throw new Error('Not Found')
}
return token
},
removePersonalAccessToken: async function (app, user, tokenId) {
const userId = typeof user === 'number' ? user : user.id
let token = await app.db.models.AccessToken.byId(tokenId)
if (token) {
if (token.ownerType === 'user' && token.ownerId === '' + userId) {
await token.destroy()
} else {
// should throw error
throw new Error('Not Authorized')
}
} else {
// should throw error
throw new Error('Not Found')
}
token = null
return token
},

// Should these only get added via forge/ee/lib/httpTokens?
createHTTPNodeToken: async function (app, project, name, scope = [''], expiresAt) {
Expand All @@ -268,46 +244,28 @@ module.exports = {
ownerId: projectId,
ownerType: 'http'
})
return { id: tok.id, token }
// Overwrite the hashed token with the plain value
const result = app.db.views.AccessToken.instanceHTTPTokenSummary(tok)
result.token = token
return result
},
updateHTTPNodeToken: async function (app, project, tokenId, scope = [''], expiresAt) {
const projectId = (project && typeof project === 'object') ? project.id : project
const id = isNaN(parseInt(tokenId)) ? tokenId : parseInt(tokenId)
const token = await app.db.models.AccessToken.byId(id)
const token = await app.db.models.AccessToken.byId(tokenId, 'http', projectId)
if (token) {
if (token.ownerType === 'http' && token.ownerId === projectId) {
token.scope = scope
if (expiresAt === undefined) {
token.expiresAt = null
} else {
token.expiresAt = expiresAt
}
await token.save()
token.scope = scope
if (expiresAt === undefined) {
token.expiresAt = null
} else {
throw new Error('Not Authorized')
token.expiresAt = expiresAt
}
await token.save()
} else {
// should throw unknown token error
throw new Error('Not Found')
}
return token
},
revokeHTTPNodeToken: async function (app, project, tokenId) {
const projectId = (project && typeof project === 'object') ? project.id : project
const id = isNaN(parseInt(tokenId)) ? tokenId : parseInt(tokenId)
let token = await app.db.models.AccessToken.byId(id)
if (token) {
if (token.ownerType === 'http' && token.ownerId === projectId) {
await token.destroy()
} else {
throw new Error('Not Authorized')
}
} else {
throw new Error('Not Found')
}
token = null
return token
},

refreshToken: async function (app, refreshToken) {
const existingToken = await app.db.models.AccessToken.byRefreshToken(refreshToken)
Expand Down
11 changes: 9 additions & 2 deletions forge/db/models/AccessToken.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,19 @@ module.exports = {
finders: function (M) {
return {
static: {
byId: async (id) => {
byId: async (id, ownerType, ownerId) => {
if (typeof id === 'string') {
id = M.AccessToken.decodeHashid(id)
}
const where = { id }
if (ownerType) {
where.ownerType = ownerType
}
if (ownerId) {
where.ownerId = '' + ownerId
}
return this.findOne({
where: { id }
where
})
},
byRefreshToken: async (refreshToken) => {
Expand Down
83 changes: 82 additions & 1 deletion forge/db/views/AccessToken.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,88 @@ module.exports = function (app) {
}
return tokenSummary
}

app.addSchema({
$id: 'PersonalAccessTokenSummary',
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
// scope: { type: 'string', nullable: true },
expiresAt: { type: 'string', nullable: true }
}
})
app.addSchema({
$id: 'PersonalAccessToken',
type: 'object',
allOf: [{ $ref: 'PersonalAccessTokenSummary' }],
properties: {
token: { type: 'string' }
}
})

function personalAccessTokenSummary (token) {
const tokenSummary = {
id: token.hashid,
name: token.name,
expiresAt: token.expiresAt
}
return tokenSummary
}
app.addSchema({
$id: 'PersonalAccessTokenSummaryList',
type: 'array',
items: {
$ref: 'PersonalAccessTokenSummary'
}
})
function personalAccessTokenSummaryList (tokenArray) {
return tokenArray.map(token => personalAccessTokenSummary(token))
}

app.addSchema({
$id: 'InstanceHTTPTokenSummary',
type: 'object',
properties: {
id: { type: 'string' },
name: { type: 'string' },
// scope: { type: 'string', nullable: true },
expiresAt: { type: 'string', nullable: true }
}
})
app.addSchema({
$id: 'InstanceHTTPToken',
type: 'object',
allOf: [{ $ref: 'InstanceHTTPTokenSummary' }],
properties: {
token: { type: 'string' }
}
})

function instanceHTTPTokenSummary (token) {
const tokenSummary = {
id: token.hashid,
name: token.name,
expiresAt: token.expiresAt
}
return tokenSummary
}
app.addSchema({
$id: 'InstanceHTTPTokenSummaryList',
type: 'array',
items: {
$ref: 'InstanceHTTPTokenSummary'
}
})
function instanceHTTPTokenSummaryList (tokenArray) {
return tokenArray.map(token => instanceHTTPTokenSummary(token))
}

return {
provisioningTokenSummary
provisioningTokenSummary,
personalAccessTokenSummary,
personalAccessTokenSummaryList,
instanceHTTPTokenSummary,
instanceHTTPTokenSummaryList
}
}
41 changes: 25 additions & 16 deletions forge/ee/routes/httpTokens/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,10 @@ module.exports = async function (app) {
preHandler: app.needsPermission('project:edit')
}, async (request, reply) => {
const tokens = await app.db.models.AccessToken.getProjectHTTPTokens(request.project)
reply.send(tokens || {})
reply.send({
tokens: app.db.views.AccessToken.instanceHTTPTokenSummaryList(tokens),
count: tokens.length
})
})

app.post('/', {
Expand All @@ -49,6 +52,7 @@ module.exports = async function (app) {
try {
const body = request.body
const token = await app.db.controllers.AccessToken.createHTTPNodeToken(request.project, body.name, body.scope, body.expiresAt)
// token has already been sanitised via views.AccessToken.instanceHTTPTokenSummary
await app.auditLog.Project.project.httpToken.created(request.session.User, null, request.project, body)
reply.send(token || {})
} catch (err) {
Expand All @@ -58,18 +62,20 @@ module.exports = async function (app) {
})

app.put('/:id', {
preHandler: app.needsPermission('project:edit')
preHandler: app.needsPermission('project:edit', true)
}, async (request, reply) => {
const updates = new app.auditLog.formatters.UpdatesCollection()
try {
const id = isNaN(parseInt(request.params.id)) ? request.params.id : parseInt(request.params.id)
const oldToken = await app.db.models.AccessToken.byId(id)
const body = request.body
const token = await app.db.controllers.AccessToken.updateHTTPNodeToken(request.project, request.params.id, body.scope, body.expiresAt)
updates.pushDifferences({ expiresAt: oldToken.expiresAt, scope: oldToken.scope.join(',') }, { expiresAt: body.expiresAt, scope: body.scope })
await app.auditLog.Project.project.httpToken.updated(request.session.User, null, request.project, updates)
delete token.token
reply.send(token || {})
const oldToken = await app.db.models.AccessToken.byId(request.params.id, 'http', request.project.id)
if (oldToken) {
const body = request.body
const token = await app.db.controllers.AccessToken.updateHTTPNodeToken(request.project, request.params.id, body.scope, body.expiresAt)
const updates = new app.auditLog.formatters.UpdatesCollection()
updates.pushDifferences({ expiresAt: oldToken.expiresAt, scope: oldToken.scope.join(',') }, { expiresAt: body.expiresAt, scope: body.scope })
await app.auditLog.Project.project.httpToken.updated(request.session.User, null, request.project, updates)
reply.send(app.db.views.AccessToken.instanceHTTPTokenSummary(token))
return
}
reply.code(404).send({ code: 'not_found', error: 'Not Found' })
} catch (err) {
const resp = { code: 'unexpected_error', error: err.toString() }
reply.code(400).send(resp)
Expand All @@ -80,11 +86,14 @@ module.exports = async function (app) {
preHandler: app.needsPermission('project:edit')
}, async (request, reply) => {
try {
const id = isNaN(parseInt(request.params.id)) ? request.params.id : parseInt(request.params.id)
const oldToken = await app.db.models.AccessToken.byId(id)
await app.db.controllers.AccessToken.revokeHTTPNodeToken(request.project, request.params.id)
await app.auditLog.Project.project.httpToken.deleted(request.session.User, null, request.project, { name: oldToken.name })
reply.code(201).send()
const oldToken = await app.db.models.AccessToken.byId(request.params.id, 'http', request.project.id)
if (oldToken) {
await oldToken.destroy()
await app.auditLog.Project.project.httpToken.deleted(request.session.User, null, request.project, { name: oldToken.name })
reply.code(201).send()
return
}
reply.code(404).send({ code: 'not_found', error: 'Not Found' })
} catch (err) {
const resp = { code: 'unexpected_error', error: err.toString() }
reply.code(400).send(resp)
Expand Down
9 changes: 6 additions & 3 deletions forge/routes/api/teamDevices.js
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ module.exports = async function (app) {
let tokenName = 'unknown'
const tokenId = request.params.tokenId
try {
const accessToken = await app.db.models.AccessToken.byId(tokenId)
const accessToken = await app.db.models.AccessToken.byId(tokenId, 'team', team.id)
if (accessToken) {
if (instanceId) {
const instanceDetails = await app.db.models.Project.findOne({
Expand Down Expand Up @@ -321,15 +321,18 @@ module.exports = async function (app) {
}, async (request, reply) => {
let tokenName = 'unknown'
const tokenId = request.params.tokenId
const team = request.team
try {
const accessToken = await app.db.models.AccessToken.byId(tokenId)
const accessToken = await app.db.models.AccessToken.byId(tokenId, 'team', team.id)
if (accessToken) {
const tokenDetails = await app.db.views.AccessToken.provisioningTokenSummary(accessToken)
tokenName = tokenDetails.name || '[unnamed]'
await accessToken.destroy()
await app.auditLog.Team.team.device.provisioning.deleted(request.session.User, null, tokenId, tokenName, request.team)
reply.send({ status: 'okay' })
return
}
reply.send({ status: 'okay' })
reply.code(404).send({ code: 'not_found', error: 'Token not found' })
} catch (err) {
const resp = { code: 'unexpected_error', error: err.toString() }
await app.auditLog.Team.team.device.provisioning.deleted(request.session.User, resp, tokenId, tokenName, request.team)
Expand Down
Loading
Loading