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

putFileArchival #248

Merged
merged 17 commits into from Aug 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 3 additions & 2 deletions hub/.eslintrc.js
Expand Up @@ -54,8 +54,9 @@ module.exports = {
"@typescript-eslint/no-angle-bracket-type-assertion": "off",
"@typescript-eslint/prefer-interface": "off",
"@typescript-eslint/no-use-before-define": "off",

"no-unused-vars": "off",
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }],
// TODO: enable this when reasonable
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-explicit-any": "off"
}
};
6 changes: 6 additions & 0 deletions hub/CHANGELOG.md
Expand Up @@ -6,6 +6,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [2.6.0]
### Added
- Implemented the `putFileArchival` restrictive auth scope which causes any
file modifications such as `POST /store/...` or `DELETE /delete/...` to
"backup" the original file by using a historical naming scheme. For example,
a file write to `{address}/foo/bar/photo.png` will cause the original
file, if it exists, to be renamed to
`{address}/foo/bar/.history.{timestamp}.{guid}.photo.png`.
- The `/list-files/${address}` endpoint now returns file metadata
(last modified date, content length) if the `POST` body contains
a `stat: true` option.
Expand Down
16 changes: 15 additions & 1 deletion hub/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion hub/package.json
Expand Up @@ -20,6 +20,7 @@
"fs-extra": "^8.1.0",
"jsontokens": "^2.0.2",
"lru-cache": "^5.1.1",
"nanoid": "^2.0.3",
"node-fetch": "^2.6.0",
"winston": "^3.2.1"
},
Expand All @@ -30,6 +31,7 @@
"@types/fetch-mock": "^7.3.1",
"@types/fs-extra": "^8.0.0",
"@types/lru-cache": "^5.1.0",
"@types/nanoid": "^2.0.0",
"@types/node": "^10.14.10",
"@types/node-fetch": "^2.3.7",
"@types/proxyquire": "^1.3.28",
Expand Down Expand Up @@ -58,7 +60,7 @@
"dev": "ts-node src/index.ts",
"build": "tsc && chmod +x lib/index.js && npm run build-schema",
"build-schema": "typescript-json-schema tsconfig.json HubConfig --required --noExtraProps --refs=false -o config-schema.json",
"lint": "eslint --ext .ts ./src",
"lint": "eslint --ext .ts ./src -f unix",
"test": "npm run build && npm run lint && NODE_ENV=test nyc node ./test/src/index.ts"
},
"repository": {
Expand Down
114 changes: 86 additions & 28 deletions hub/src/server/authentication.ts
Expand Up @@ -13,29 +13,58 @@ function pubkeyHexToECPair (pubkeyHex: string) {
return bitcoinjs.ECPair.fromPublicKey(pkBuff)
}

export type AuthScopeType = {
scope: string,
export interface AuthScopeEntry {
scope: string
domain: string
}

export type TokenPayloadType = {
gaiaChallenge: string,
iss: string,
exp: number,
iat?: number,
salt: string,
hubUrl?: string,
associationToken?: string,
scopes?: AuthScopeType[],
export interface TokenPayloadType {
gaiaChallenge: string
iss: string
exp: number
iat?: number
salt: string
hubUrl?: string
associationToken?: string
scopes?: AuthScopeEntry[]
childToAssociate?: string
}

export const AuthScopes = [
'putFile',
'putFilePrefix',
'deleteFile',
'deleteFilePrefix'
]
export class AuthScopeValues {

writePrefixes: string[] = []
writePaths: string[] = []
deletePrefixes: string[] = []
deletePaths: string[] = []
writeArchivalPrefixes: string[] = []
writeArchivalPaths: string[] = []

static parseEntries(scopes: AuthScopeEntry[]) {
const scopeTypes = new AuthScopeValues()
scopes.forEach(entry => {
switch (entry.scope) {
case AuthScopesTypes.putFilePrefix: return scopeTypes.writePrefixes.push(entry.domain)
case AuthScopesTypes.putFile: return scopeTypes.writePaths.push(entry.domain)
case AuthScopesTypes.putFileArchival: return scopeTypes.writeArchivalPaths.push(entry.domain)
case AuthScopesTypes.putFileArchivalPrefix: return scopeTypes.writeArchivalPrefixes.push(entry.domain)
case AuthScopesTypes.deleteFilePrefix: return scopeTypes.deletePrefixes.push(entry.domain)
case AuthScopesTypes.deleteFile: return scopeTypes.deletePaths.push(entry.domain)
}
})
return scopeTypes
}
}

export class AuthScopesTypes {
static readonly putFile = 'putFile'
static readonly putFilePrefix = 'putFilePrefix'
static readonly deleteFile = 'deleteFile'
static readonly deleteFilePrefix = 'deleteFilePrefix'
static readonly putFileArchival = 'putFileArchival'
static readonly putFileArchivalPrefix = 'putFileArchivalPrefix'
}

export const AuthScopeTypeArray: string[] = Object.values(AuthScopesTypes).filter(val => typeof val === 'string')

export function getTokenPayload(token: import('jsontokens/lib/decode').TokenInterface) {
if (typeof token.payload === 'string') {
Expand All @@ -59,7 +88,22 @@ export function decodeTokenForPayload(opts: {
}
}

export class V1Authentication {
export interface AuthenticationInterface {
checkAssociationToken(token: string, bearerAddress: string): void
getAuthenticationScopes(): AuthScopeEntry[]
isAuthenticationValid(
address: string,
challengeTexts: string[],
options?: {
requireCorrectHubUrl?: boolean,
validHubUrls?: string[],
oldestValidTokenTimestamp?: number
}
): string
parseAuthScopes(): AuthScopeValues
}

export class V1Authentication implements AuthenticationInterface {
token: string

constructor(token: string) {
Expand Down Expand Up @@ -90,7 +134,7 @@ export class V1Authentication {
}

static makeAuthPart(secretKey: bitcoinjs.ECPairInterface, challengeText: string,
associationToken?: string, hubUrl?: string, scopes?: Array<AuthScopeType>,
associationToken?: string, hubUrl?: string, scopes?: AuthScopeEntry[],
issuedAtDate?: number) {

const FOUR_MONTH_SECONDS = 60 * 60 * 24 * 31 * 4
Expand Down Expand Up @@ -188,6 +232,11 @@ export class V1Authentication {

}

parseAuthScopes() {
const scopes = this.getAuthenticationScopes()
return AuthScopeValues.parseEntries(scopes)
}

/*
* Get the authentication token's association token's scopes.
* Does not validate the authentication token or the association token
Expand All @@ -196,7 +245,7 @@ export class V1Authentication {
* Returns the scopes, if there are any given.
* Returns [] if there is no association token, or if the association token has no scopes
*/
getAuthenticationScopes(): Array<AuthScopeType> {
getAuthenticationScopes() {

const payload = decodeTokenForPayload({
encodedToken: this.token,
Expand All @@ -211,7 +260,7 @@ export class V1Authentication {
}

// unambiguously convert to AuthScope
const scopes = payload.scopes.map((s: any) => {
const scopes: AuthScopeEntry[] = payload.scopes.map((s: any) => {
const r = {
scope: String(s.scope),
domain: String(s.domain)
Expand Down Expand Up @@ -336,7 +385,17 @@ export class V1Authentication {
}
}

export class LegacyAuthentication {
export class LegacyAuthentication implements AuthenticationInterface {

checkAssociationToken(_token: string, _bearerAddress: string): void {
throw new Error('Method not implemented.')
}

parseAuthScopes(): AuthScopeValues {
// TODO: can legacy auth tokens be maliciously created to get around restrictive auth scopes?
zone117x marked this conversation as resolved.
Show resolved Hide resolved
return new AuthScopeValues()
}

publickey: bitcoinjs.ECPairInterface
signature: string
constructor(publickey: bitcoinjs.ECPairInterface, signature: string) {
Expand Down Expand Up @@ -368,7 +427,7 @@ export class LegacyAuthentication {
return Buffer.from(JSON.stringify(authObj)).toString('base64')
}

getAuthenticationScopes(): Array<AuthScopeType> {
getAuthenticationScopes(): AuthScopeEntry[] {
// no scopes supported in this version
return []
}
Expand Down Expand Up @@ -408,7 +467,7 @@ export function getLegacyChallengeTexts(myURL: string = DEFAULT_STORAGE_URL): Ar
[header, year, myURL, myChallenge]))
}

export function parseAuthHeader(authHeader: string) {
export function parseAuthHeader(authHeader: string): AuthenticationInterface {
if (!authHeader || !authHeader.toLowerCase().startsWith('bearer')) {
throw new ValidationError('Failed to parse authentication header.')
}
Expand Down Expand Up @@ -467,17 +526,16 @@ export function validateAuthorizationHeader(authHeader: string, serverName: stri
*/
export function getAuthenticationScopes(authHeader: string) {
const authObject = parseAuthHeader(authHeader)
return authObject.getAuthenticationScopes()
return authObject.parseAuthScopes()
}


/*
* Validate authentication scopes. They must be well-formed,
* and there can't be too many of them.
* Return true if valid.
* Throw ValidationError on error
*/
function validateScopes(scopes: Array<AuthScopeType>) {
function validateScopes(scopes: AuthScopeEntry[]) {
if (scopes.length > 8) {
throw new ValidationError('Too many authentication scopes')
}
Expand All @@ -486,7 +544,7 @@ function validateScopes(scopes: Array<AuthScopeType>) {
const scope = scopes[i]

// valid scope?
const found = AuthScopes.find((s) => (s === scope.scope))
const found = AuthScopeTypeArray.find((s) => (s === scope.scope))
if (!found) {
throw new ValidationError(`Unrecognized scope ${scope.scope}`)
}
Expand Down
2 changes: 1 addition & 1 deletion hub/src/server/http.ts
Expand Up @@ -73,7 +73,7 @@ export function makeHttpServer(config: HubConfigInterface): { app: express.Appli
} else if (err instanceof errors.NotEnoughProofError) {
writeResponse(res, { message: err.message, error: err.name }, 402)
} else if (err instanceof errors.ConflictError) {
writeResponse(res, { message: err.message, error: err.name }, 409)
writeResponse(res, { message: err.message, error: err.name }, 409)
} else {
writeResponse(res, { message: 'Server Error' }, 500)
}
Expand Down