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

API Sign: Change the S3 Provider into a signer #1698

Merged
merged 20 commits into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from 12 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
29 changes: 26 additions & 3 deletions mod/provider/_provider.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
/**
@module /provider

@description
Functions for handling 3rd party service provider requests
*/

const file = require('./file')
Expand All @@ -8,19 +11,39 @@ const cloudfront = require('./cloudfront')

const s3 = require('./s3')

module.exports = async (req, res) => {
/**
@function provider
@async

@description
The provider method looks up a provider module method matching the provider request parameter and passes the req/res objects as argument to the matched method.

The response from the method is returned with the HTTP response.

@param {Object} req HTTP request.
@param {Object} res HTTP response.
@param {Object} req.params Request parameter.
@param {string} params.signer Provider module to handle the request.

@returns {Promise} The promise resolves into the response from the provider modules method.
*/
module.exports = async function provider(req, res){

const provider = {
cloudfront,
file,
s3
s3: {deprecated: true, fn: s3}
}

if (provider[req.params.provider]?.deprecated){
return await provider[req.params.provider].fn(req, res)
}

if (!Object.hasOwn(provider, req.params.provider)) {
return res.send(`Failed to validate 'provider' param.`)
}

const response = await provider[req.params.provider](req)
const response = await provider[req.params.provider](req, res)

req.params.content_type && res.setHeader('content-type', req.params.content_type)

Expand Down
144 changes: 43 additions & 101 deletions mod/provider/s3.js
Original file line number Diff line number Diff line change
@@ -1,102 +1,44 @@
/**
@module /provider/s3
AlexanderGeere marked this conversation as resolved.
Show resolved Hide resolved
*/

const {
S3Client,
PutObjectCommand,
GetObjectCommand,
DeleteObjectCommand,
ListObjectsCommand
} = require('@aws-sdk/client-s3');

const { getSignedUrl } = require('@aws-sdk/s3-request-presigner');

module.exports = async (req, res) => {

const credentials = Object.fromEntries(new URLSearchParams(process.env.AWS_S3_CLIENT))

const s3Client = new S3Client({
credentials,
region: req.params.region
})


const commands = {
get,
put,
trash,
list
}

if (!Object.hasOwn(commands, req.params.command)) {
return res.status(400).send(`S3 command validation failed.`)
}

return commands[req.params.command](s3Client, req)
}

async function trash(s3Client, req) {

const command = new DeleteObjectCommand({
Key: req.params.key,
Bucket: req.params.bucket
})

return s3Client.send(command);
}

async function get(s3Client, req) {

try {

const command = new GetObjectCommand({
Key: req.params.key,
Bucket: req.params.bucket
})

const signedURL = await getSignedUrl(s3Client, command, {
expiresIn: 3600,
});

return JSON.stringify(signedURL);

} catch (err) {
console.error(err)
}
}

async function put(s3Client, req) {

try {

const command = new PutObjectCommand({
Key: req.params.key,
Bucket: req.params.bucket,
Region: req.params.region
});

const signedURL = await getSignedUrl(s3Client, command, {
expiresIn: 3600,
});

return JSON.stringify(signedURL);

} catch (err) {
console.error(err)
}
}

async function list(s3Client, req) {

try {

const command = new ListObjectsCommand({ Bucket: req.params.bucket })
const response = await s3Client.send(command);

return response

} catch (err) {
console.error(err)
}
/**
@module /provider/s3
@deprecated

@description
Provides a way to call get, put, trash and list. Directly interfaces with s3

This module has been deprecated and replaced with {@link module:/sign/s3~s3}
*/

/**
@function s3
@async

@description
The s3 provider method will redirect requests to the signer.

Provides methods for list, get, trash and put

@param {Object} req HTTP request.
@param {Object} res HTTP response.
@param {Object} req.params Request parameter.

@returns {Request} A redirect to the s3 signer.
**/
module.exports = async function s3(req, res) {

const commands = {
get,
put,
trash,
list
}

if (!Object.hasOwn(commands, req.params.command)) {
return res.status(400).send(`S3 command validation failed.`)
}

const paramString = Object.keys(req.params).filter(param => ['command','bucket','key','region'].includes(param)).map(param => `${param}=${req.params[param]}`).join('&')

res.setHeader('location', `${process.env.DIR}/api/sign/s3?${paramString}`)

return res.status(301).send()
}
8 changes: 5 additions & 3 deletions mod/sign/_sign.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ The sign module provides access to different request signer methods.
*/

const cloudinary = require('./cloudinary')
const s3 = require('./s3')

const signerModules = {
cloudinary
cloudinary,
s3
}

/**
Expand All @@ -32,8 +34,8 @@ The response from the method is returned with the HTTP response.
*/
module.exports = async function signer(req, res) {

if (!Object.hasOwn(signerModules, req.params.signer)) {
return res.send(`Failed to validate 'provider' param.`)
if (!Object.hasOwn(signerModules, req.params.signer) || !signerModules[req.params.signer]) {
return res.send(`Failed to validate 'sign' param.`)
}

const response = await signerModules[req.params.signer](req, res)
Expand Down
149 changes: 149 additions & 0 deletions mod/sign/s3.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
/**
@module /sign/s3
@requires aws-sdk/client-s3
@requires aws-sdk/s3-request-presigner
Signs requests to S3. Provides functions for get, list, delete and put to S3.
*/

//The S3 packages are optional.
//Need a temporary assignment to determine if they are available.
let
S3Client,
PutObjectCommand,
GetObjectCommand,
DeleteObjectCommand,
ListObjectsCommand,
getSignedUrl;

//Check for credentials
if (!process.env?.AWS_S3_CLIENT) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How do we handle public buckets that don't require any credentials?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll test what happens with public buckets

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Public buckets can just be hit on the url. 7071f1b provides a function that returns the public url if no credentials are supplied

console.log('S3 Sign: Missing credentials from env: AWS_S3_CLIENT')
module.exports = null
}
else {

//Attempt import if credentials are found
try {

//Assign constructors and functions from the sdks.
const clientSDK = require('@aws-sdk/client-s3');
getSignedUrl = require('@aws-sdk/s3-request-presigner').getSignedUrl;

S3Client = clientSDK.S3Client
PutObjectCommand = clientSDK.PutObjectCommand
GetObjectCommand = clientSDK.GetObjectCommand
DeleteObjectCommand = clientSDK.DeleteObjectCommand
ListObjectsCommand = clientSDK.ListObjectsCommand

//Export the function .
module.exports = s3
}
catch (err) {

//The module has not been installed.
if (err.code === 'MODULE_NOT_FOUND') {
console.log('AWS-SDK is not available')
module.exports = null
}
else throw err
}
}


/**
@function s3
@async

@description
The s3 signer method signs requests for the s3 service.

Provides methods for list, get, trash and put

@param {Object} req HTTP request.
@param {Object} res HTTP response.
@param {Object} req.params Request parameter.
@param {string} params.region
@param {string} params.bucket
@param {string} params.key
@param {string} params.command

@returns {Promise<String>} The signed url associated to the request params.
**/
async function s3(req, res) {

//Read credentials from an env key
const credentials = Object.fromEntries(new URLSearchParams(process.env.AWS_S3_CLIENT))

const s3Client = new S3Client({
credentials,
region: req.params.region
})

req.params.s3Client = s3Client

//Assign the corresponding function to the requested command
const commands = {
get: () => objectAction(req.params, GetObjectCommand),
trash: () => objectAction(req.params, DeleteObjectCommand),
put: () => objectAction(req.params, PutObjectCommand),
list: () => objectAction(req.params, ListObjectsCommand)
}

if (!Object.hasOwn(commands, req.params.command)) {
return res.status(400).send(`S3 command validation failed.`)
}

return commands[req.params.command]()
}

/**
@function objectAction
@async

@description
Generates the signed url for the command and parameters specified from the request

@param {Function} objectCommand The S3 function to be carried out.
@param {Object} reqParams Request parameters.
@property {string} reqParams.region
@property {string} reqParams.bucket
@property {string} reqParams.key
@property {string} reqParams.command

@returns {String} The signed url associated to the request params.
**/
async function objectAction(reqParams, objectCommand) {
AlexanderGeere marked this conversation as resolved.
Show resolved Hide resolved

//The parameters required per action for S3
//S3 Parameters are capitalised
const actionParams = {
get: { 'key': 'Key', 'bucket': 'Bucket' },
list: { 'bucket': 'Bucket' },
put: { 'key': 'Key', 'bucket': 'Bucket', 'region': 'Region' },
trash: { 'key': 'Key', 'bucket': 'Bucket' }
}

try {

//Transfrom our keys into aws key names
const commandParams = Object.keys(reqParams)
.filter(key => Object.keys(actionParams[reqParams.command]).includes(key))
.reduce((acc, key) => ({
...acc,
...{ [actionParams[reqParams.command][key]]: reqParams[key] }
}),
{}
)

const command = new objectCommand(commandParams)

const signedURL = await getSignedUrl(reqParams.s3Client, command, {
expiresIn: 3600,
});

return JSON.stringify(signedURL);

} catch (err) {
console.error(err)
}
}
6 changes: 4 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@
"test-docs": "jsdoc --configure jsdoc_test.json --verbose"
},
"license": "MIT",
"dependencies": {
"optionalDependencies": {
RobAndrewHurst marked this conversation as resolved.
Show resolved Hide resolved
"@aws-sdk/client-s3": "^3.621.0",
"@aws-sdk/s3-request-presigner": "^3.621.0"
},
"dependencies": {
"@aws-sdk/cloudfront-signer": "^3.621.0",
"@aws-sdk/s3-request-presigner": "^3.621.0",
"jsonwebtoken": "^9.0.2",
"nodemailer": "^6.9.7",
"pg": "^8.7.3",
Expand Down
Loading