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

Improvement/arsn 414 post object #2243

Draft
wants to merge 3 commits into
base: development/7.70
Choose a base branch
from
Draft
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
18 changes: 16 additions & 2 deletions lib/auth/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,13 @@ const checkFunctions = {
v2: {
headers: v2.header.check,
query: v2.query.check,
// TODO ARSN-414 check v2 auth for POST requests with form data
// form: v2.form.check,
},
v4: {
headers: v4.header.check,
query: v4.query.check,
form: v4.form.check,
},
};

Expand Down Expand Up @@ -63,7 +66,7 @@ function extractParams(
log.trace('entered', { method: 'Arsenal.auth.server.extractParams' });
const authHeader = request.headers.authorization;
let version: 'v2' |'v4' | null = null;
let method: 'query' | 'headers' | null = null;
let method: 'query' | 'headers' | 'form' | null = null;

// Identify auth version and method to dispatch to the right check function
if (authHeader) {
Expand All @@ -85,6 +88,16 @@ function extractParams(
} else if (data['X-Amz-Algorithm']) {
method = 'query';
version = 'v4';
} if (data.Policy) {
if (data['X-Amz-Algorithm']) {
method = 'form';
version = 'v4';
}
// TODO ARSN-414 check v2 auth for POST requests with form data
// if (formData.Signature) {
// method = 'form';
// version = 'v2';
// }
}

// Here, either both values are set, or none is set
Expand Down Expand Up @@ -121,7 +134,8 @@ function doAuth(
awsService: string,
requestContexts: any[] | null
) {
const res = extractParams(request, log, awsService, request.query);
const data: { [key: string]: string; } = request.formData || request.query || {};
const res = extractParams(request, log, awsService, data);
if (res.err) {
return cb(res.err);
} else if (res.params instanceof AuthInfo) {
Expand Down
1 change: 1 addition & 0 deletions lib/auth/v4/authV4.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * as header from './headerAuthCheck';
export * as query from './queryAuthCheck';
export * as form from './formAuthCheck';
187 changes: 187 additions & 0 deletions lib/auth/v4/formAuthCheck.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { Logger } from 'werelogs';
import * as constants from '../../constants';
import errors from '../../errors';
import constructStringToSign from './constructStringToSign';
import { checkTimeSkew, convertAmzTimeToMs } from './timeUtils';
import { validateCredentials, extractFormParams } from './validateInputs';
import { areSignedHeadersComplete } from './validateInputs';

/**
* V4 query auth check
* @param request - HTTP request object
* @param log - logging object
* @param data - Contain authentification params (GET or POST data)
*/
export function check(request: any, log: Logger, data: { [key: string]: string }) {
const authParams = extractFormParams(data, log);

if (Object.keys(authParams).length !== 4) {
return { err: errors.InvalidArgument };
}

// Query params are not specified in AWS documentation as case-insensitive,
// so we use case-sensitive
const token = data['X-Amz-Security-Token'];
if (token && !constants.iamSecurityToken.pattern.test(token)) {
log.debug('invalid security token', { token });
return { err: errors.InvalidToken };
}

const signedHeaders = authParams.signedHeaders!;
const signatureFromRequest = authParams.signatureFromRequest!;
const timestamp = authParams.timestamp!;
//const expiry = authParams.expiry!;
const credential = authParams.credential!;

if (!areSignedHeadersComplete(signedHeaders, request.headers)) {
log.debug('signedHeaders are incomplete', { signedHeaders });
return { err: errors.AccessDenied };
}

const validationResult = validateCredentials(credential, timestamp,
log);
if (validationResult instanceof Error) {
log.debug('credentials in improper format', { credential,
timestamp, validationResult });
return { err: validationResult };
}
const accessKey = credential[0];
const scopeDate = credential[1];
const region = credential[2];
const service = credential[3];
const requestType = credential[4];

// In query v4 auth, the canonical request needs
// to include the query params OTHER THAN
// the signature so create a
// copy of the query object and remove
// the X-Amz-Signature property.
const queryWithoutSignature = Object.assign({}, data);
delete queryWithoutSignature['X-Amz-Signature'];

// For query auth, instead of a
// checksum of the contents, the
// string 'UNSIGNED-PAYLOAD' should be
// added to the canonicalRequest in
// building string to sign
const payloadChecksum = 'UNSIGNED-PAYLOAD';

// string to sign is the policy
const stringToSign = data['Policy'];
log.trace('constructed stringToSign', { stringToSign });
return {
err: null,
params: {
version: 4,
data: {
accessKey,
signatureFromRequest,
region,
scopeDate,
stringToSign,
service,
authType: 'REST-QUERY-STRING',
signatureVersion: 'AWS4-HMAC-SHA256',
signatureAge: Date.now() - convertAmzTimeToMs(timestamp),
securityToken: token,
},
},
};
}

// /**
// * V4 form auth check for POST Object request
// * @param request - HTTP request object containing form data
// * @param log - logging object
// */
// export function check(request: any, log: Logger, formData: { [key: string]: string }) {
// // Assume form data is already parsed and attached to request.body

// // Extract authentication parameters from formData
// const algorithm = formData['X-Amz-Algorithm'];
// const credentials = formData['X-Amz-Credential'];
// const date = formData['X-Amz-Date'];
// const securityToken = formData['X-Amz-Security-Token'];
// const signature = formData['X-Amz-Signature'];

// let splitCredentials : [string, string, string, string, string];
// if (credentials && credentials.length > 28 && credentials.indexOf('/') > -1) {
// // @ts-ignore
// splitCredentials = credentials.split('/');
// } else {
// log.debug('invalid credential param', { credentials,
// date });
// return { err: errors.InvalidArgument };
// }

// if (!algorithm || !splitCredentials || !date || !signature) {
// return { err: errors.InvalidArgument };
// }

// // Validate the token if present
// if (securityToken && !constants.iamSecurityToken.pattern.test(securityToken)) {
// log.debug('invalid security token', { token: securityToken });
// return { err: errors.InvalidToken };
// }

// // Checking credential format
// const validationResult = validateCredentials(splitCredentials, date,
// log);
// if (validationResult instanceof Error) {
// log.debug('credentials in improper format', { splitCredentials,
// date, validationResult });
// return { err: validationResult };
// }

// const accessKey = splitCredentials[0];
// const scopeDate = splitCredentials[1];
// const region = splitCredentials[2];
// const service = splitCredentials[3];
// const requestType = splitCredentials[4];

// // Verifying the timestamp and potential expiration
// const isTimeSkewed = checkTimeSkew(date, request.expiry, log);
// if (isTimeSkewed) {
// return { err: errors.RequestTimeTooSkewed };
// }

// // Extract signed headers
// const signedHeaders = Object.keys(request.headers).map(key => key.toLowerCase()).sort().join(';');


// const stringToSign = constructStringToSign({
// request,
// signedHeaders,
// payloadChecksum: null,
// credentialScope:
// `${scopeDate}/${region}/${service}/${requestType}`,
// timestamp: date,
// query: formData,
// log,
// awsService: service,
// });
// if (stringToSign instanceof Error) {
// return { err: stringToSign };
// }
// log.trace('constructed stringToSign', { stringToSign });

// // If all checks are successful
// return {
// err: null,
// params: {
// version: 4,
// data: {
// accessKey: accessKey,
// signatureFromRequest: signature,
// date: date,
// region: region,
// scopeDate: scopeDate,
// stringToSign: stringToSign,
// authType: 'POST-OBJECT',
// signatureVersion: 'AWS4-HMAC-SHA256',
// signatureAge: Date.now() - convertAmzTimeToMs(date),
// securityToken: securityToken,
// }
// }
// };
// }
67 changes: 67 additions & 0 deletions lib/auth/v4/validateInputs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,73 @@ export function extractQueryParams(
}


/**
* Extract and validate components from formData object
* @param formObj - formData object from request
* @param log - logging object
* @return object containing extracted query params for authV4
*/
export function extractFormParams(
formObj: { [key: string]: string | undefined },
log: Logger
) {
const authParams: {
signedHeaders?: string;
signatureFromRequest?: string;
timestamp?: string;
expiry?: number;
credential?: [string, string, string, string, string];
} = {};

// Do not need the algorithm sent back
if (formObj['X-Amz-Algorithm'] !== 'AWS4-HMAC-SHA256') {
log.warn('algorithm param incorrect',
{ algo: formObj['X-Amz-Algorithm'] });
return authParams;
}

// adding placeholder for signedHeaders to satisfy Vault
// as this is not required for form auth
authParams.signedHeaders = 'content-type;host;x-amz-date;x-amz-security-token';

const signature = formObj['X-Amz-Signature'];
if (signature && signature.length === 64) {
authParams.signatureFromRequest = signature;
} else {
log.warn('missing signature');
return authParams;
}

const timestamp = formObj['X-Amz-Date'];
if (timestamp && timestamp.length === 16) {
authParams.timestamp = timestamp;
} else {
log.warn('missing or invalid timestamp',
{ timestamp: formObj['X-Amz-Date'] });
return authParams;
}

// TODO? ARSN-414 Does not seem to be required for form auth
// const expiry = Number.parseInt(formObj['X-Amz-Expires'] ?? 'nope', 10);
// const sevenDays = 604800;
// if (expiry && (expiry > 0 && expiry <= sevenDays)) {
// authParams.expiry = expiry;
// } else {
// log.warn('invalid expiry', { expiry });
// return authParams;
// }

const credential = formObj['X-Amz-Credential'];
if (credential && credential.length > 28 && credential.indexOf('/') > -1) {
// @ts-ignore
authParams.credential = credential.split('/');
} else {
log.warn('invalid credential param', { credential });
return authParams;
}
return authParams;
}

/**
* Extract and validate components from auth header
* @param authHeader - authorization header from request
Expand Down
4 changes: 4 additions & 0 deletions lib/s3routes/routes/routePOST.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,10 @@ export default function routePOST(
corsHeaders));
}

if (objectKey === undefined && Object.keys(query).length === 0) {
return api.callApiMethod('objectPost', request, response, log, (err, resHeaders) => routesUtils.responseNoBody(err, resHeaders, response, 200, log));
}

return routesUtils.responseNoBody(errors.NotImplemented, null, response,
200, log);
}
Loading