Skip to content

Commit

Permalink
Add support for token based auth to services other than VSTS (#6440)
Browse files Browse the repository at this point in the history
This fixes #4956
Also fixes #6411
  • Loading branch information
keithrob authored Feb 21, 2018
1 parent 1bde6b9 commit 193a231
Show file tree
Hide file tree
Showing 7 changed files with 312 additions and 19 deletions.
77 changes: 62 additions & 15 deletions Tasks/Common/npm-common/npmregistry.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as os from 'os';
import * as tl from 'vsts-task-lib/task';
import * as url from 'url';
import * as URL from 'url';
import * as ipaddress from 'ip-address';

import { NormalizeRegistry } from './npmrcparser';
import * as util from './util';
Expand All @@ -23,19 +24,39 @@ export class NpmRegistry implements INpmRegistry {
}

public static FromServiceEndpoint(endpointId: string, authOnly?: boolean): NpmRegistry {
let email: string;
let username: string;
let password: string;
let lineEnd = os.EOL;
let endpointAuth: tl.EndpointAuthorization;
let url: string;
let nerfed: string;
let auth: string;
let username: string;
let password: string;
let email: string;
let password64: string;
let isVstsTokenAuth: boolean = false;
try {
endpointAuth = tl.getEndpointAuthorization(endpointId, false);
} catch (exception) {
throw new Error(tl.loc('ServiceEndpointNotDefined'));
}

try {
let collectionUrl = tl.getVariable("System.TeamFoundationCollectionUri");
let collectionUrlDomain = NpmRegistry.ParseHostnameForDomain(URL.parse(collectionUrl).hostname);
url = NormalizeRegistry(tl.getEndpointUrl(endpointId, false));
let epUrlDomain = NpmRegistry.ParseHostnameForDomain(URL.parse(url).hostname);

// To the reader, this could be optimized here but it is broken out for readability
if (endpointAuth.scheme === 'Token'){
if (collectionUrlDomain === epUrlDomain){
// Same domain _OR_ matching IP addrs. Use PAT+Basic
isVstsTokenAuth = true;
} else if (epUrlDomain.toUpperCase() === 'VISUALSTUDIO.COM'){
// If the endpoint is VSTS, full stop. Use PAT+Basic
isVstsTokenAuth = true;
}
}
nerfed = util.toNerfDart(url);
} catch (exception) {
throw new Error(tl.loc('ServiceEndpointUrlNotDefined'));
}
Expand All @@ -45,23 +66,32 @@ export class NpmRegistry implements INpmRegistry {
username = endpointAuth.parameters['username'];
password = endpointAuth.parameters['password'];
email = username; // npm needs an email to be set in order to publish, this is ignored on npmjs
password64 = (new Buffer(password).toString('base64'));

auth = nerfed + ":username=" + username + lineEnd;
auth += nerfed + ":_password=" + password64 + lineEnd;
auth += nerfed + ":email=" + email + lineEnd;
break;
case 'Token':
email = 'VssEmail';
username = 'VssToken';
password = endpointAuth.parameters['apitoken'];
let apitoken = endpointAuth.parameters['apitoken'];
if (!isVstsTokenAuth){
// Use Bearer auth as it was intended.
auth = nerfed + ":_authToken=" + apitoken + lineEnd;
}else{
// VSTS does not support PATs+Bearer only JWTs+Bearer
email = 'VssEmail';
username = 'VssToken';
password64 = (new Buffer(apitoken).toString('base64'));
console.log("##vso[task.setvariable variable=" + endpointId + "BASE64_PASSWORD;issecret=true;]" + password64);

auth = nerfed + ":username=" + username + lineEnd;
auth += nerfed + ":_password=" + password64 + lineEnd;
auth += nerfed + ":email=" + email + lineEnd;
}
break;
}
let lineEnd = os.EOL;
let nerfed = util.toNerfDart(url);
let password64 = (new Buffer(password).toString('base64'));
console.log("##vso[task.setvariable variable=" + endpointId + "BASE64_PASSWORD;issecret=true;]" + password64);

let auth = nerfed + ":username=" + username + lineEnd;
auth += nerfed + ":_password=" + password64 + lineEnd;
auth += nerfed + ":email=" + email + lineEnd;
auth += nerfed + ":always-auth=true";

return new NpmRegistry(url, auth, authOnly);
}

Expand All @@ -72,4 +102,21 @@ export class NpmRegistry implements INpmRegistry {

return new NpmRegistry(url, auth, authOnly);
}

/**
* Helper function to return the domain name given a hostname.
* @param hostname This should be the output from: url.parse('http://myserver.example.com').hostname
* @returns Given a FQDN the domain name will be returned. Given a hostname the
* hostname will be return. Given an IP address, the IP will be returned.
*/
private static ParseHostnameForDomain(hostname: string): string {
if (hostname &&
(!new ipaddress.Address6(hostname).isValid() &&
!new ipaddress.Address4(hostname).isValid())) {
// We know we have a non-null string that is not an IP addr
let hnAry = hostname.split('.');
return hnAry.slice(-2).join('.')
}
return hostname;
}
}
49 changes: 49 additions & 0 deletions Tasks/Common/npm-common/package-lock.json

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

1 change: 1 addition & 0 deletions Tasks/Common/npm-common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"dependencies": {
"ini": "^1.3.4",
"q": "^1.5.0",
"ip-address": "^5.8.9",
"vsts-task-lib": "2.0.6",
"vso-node-api": "^6.2.2-preview"
}
Expand Down
196 changes: 196 additions & 0 deletions Tasks/Npm/Tests/L0.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ import * as ttm from 'vsts-task-lib/mock-test';

import { NpmMockHelper } from './NpmMockHelper';

const BASIC_AUTH_PAT_PASSWD_REGEX = /\/\/.*\/:_password=.*/g;
const BEARER_AUTH_REGEX = /\/\/.*\/:_authToken=AUTHTOKEN.*/g;
const BASIC_AUTH_PAT_EML_REGEX = /\/\/.*\/:email=VssEmail.*/g;
const BASIC_AUTH_PAT_USERNAME_REGEX = /\/\/.*\/:username=VssToken.*/g;
const AWLAYS_AUTH_REGEX = /\/\/.*\/:always-auth=true.*/g;

describe('Npm Task', function () {
before(() => {
mockery.enable({
Expand Down Expand Up @@ -319,4 +325,194 @@ describe('Npm Task', function () {
});
});
});

it('does Basic auth for hosted when service endpoint auth is Token and endpoint is in the .visualstudio.com domain',
(done: MochaDone) => {
// Scenario: Cross account on visualstudio.com
let mockTask = {
getVariable: (v) => {
if (v === 'System.TeamFoundationCollectionUri') {
return 'http://example.visualstudio.com';
}
},
getEndpointAuthorization: (id, optional) => {
return { scheme: 'Token', parameters: { 'apitoken': 'AUTHTOKEN' } };
},
getEndpointUrl: (id, optional) => {
return 'http://serviceendpoint.visualstudio.com';
}
};
mockery.registerMock('vsts-task-lib/task', mockTask);
const npmregistry = require("npm-common/npmregistry");
let registry = npmregistry.NpmRegistry.FromServiceEndpoint('endpointId');

assert(registry.auth.match(BASIC_AUTH_PAT_PASSWD_REGEX), `Auth must contain a password. Auth is: (${registry.auth})`);
assert(registry.auth.match(BASIC_AUTH_PAT_EML_REGEX), `Auth must contain a email. Auth is: (${registry.auth})`);
assert(registry.auth.match(BASIC_AUTH_PAT_USERNAME_REGEX), `Auth must contain a email. Auth is: (${registry.auth})`);
assert(registry.auth.match(AWLAYS_AUTH_REGEX), `Auth must contain always-auth. Auth is: (${registry.auth})`);

done();
});

it('does Bearer auth for hosted when service endpoint auth is Token and endpoint is 3rd party', (done: MochaDone) => {
// Scenario: User is connecting to a non-visualstudio.com registry
let mockTask = {
getVariable: (v) => {
if (v === 'System.TeamFoundationCollectionUri') {
return 'http://example.visualstudio.com';
}
},
getEndpointAuthorization: (id, optional) => {
return { scheme: 'Token', parameters: { 'apitoken': 'AUTHTOKEN' } };
},
getEndpointUrl: (id, optional) => {
return 'http://somepublicrepo.contoso.com:8080/some/random/path';
}
};
mockery.registerMock('vsts-task-lib/task', mockTask);
const npmregistry = require("npm-common/npmregistry");
let registry = npmregistry.NpmRegistry.FromServiceEndpoint('endpointId');

assert(registry.auth.match(BEARER_AUTH_REGEX), `Auth must contain _authToken. Auth is: (${registry.auth})`);
assert(registry.auth.match(AWLAYS_AUTH_REGEX), `Auth must contain always-auth. Auth is: (${registry.auth})`);

done();
});

it('does Basic auth for onprem when service endpoint auth is Token and the endpoint is in the same domain', (done: MochaDone) => {
// Scenario: onprem server A registry/feed in to onprem server B within same domain
let mockTask = {
getVariable: (v) => {
if (v === 'System.TeamFoundationCollectionUri') {
// Any collectionuri not ending in .visualstudio.com is onprem
return 'http://mytfsserver.example.com';
}
},
getEndpointAuthorization: (id, optional) => {
return { scheme: 'Token', parameters: { 'apitoken': 'AUTHTOKEN' } };
},
getEndpointUrl: (id, optional) => {
return 'http://serviceendpoint.visualstudio.com';
}
};
mockery.registerMock('vsts-task-lib/task', mockTask);
const npmregistry = require("npm-common/npmregistry");
let registry = npmregistry.NpmRegistry.FromServiceEndpoint('endpointId');

assert(registry.auth.match(BASIC_AUTH_PAT_PASSWD_REGEX), `Auth must contain a password. Auth is: (${registry.auth})`);
assert(registry.auth.match(BASIC_AUTH_PAT_EML_REGEX), `Auth must contain a email. Auth is: (${registry.auth})`);
assert(registry.auth.match(BASIC_AUTH_PAT_USERNAME_REGEX), `Auth must contain a email. Auth is: (${registry.auth})`);
assert(registry.auth.match(AWLAYS_AUTH_REGEX), `Auth must contain always-auth. Auth is: (${registry.auth})`);

done();
});

it('does Bearer auth for onprem when service endpoint auth is Token and the endpoint is 3rd party', (done: MochaDone) => {
// Scenario: Onprem connecting to a 3rd party registry.
let mockTask = {
getVariable: (v) => {
if (v === 'System.TeamFoundationCollectionUri') {
return 'http://mytfsserver.example.com';
}
},
getEndpointAuthorization: (id, optional) => {
return { scheme: 'Token', parameters: { 'apitoken': 'AUTHTOKEN' } };
},
getEndpointUrl: (id, optional) => {
return 'http://somepublicrepo.contoso.com:8080/some/random/path';
}
};
mockery.registerMock('vsts-task-lib/task', mockTask);
const npmregistry = require("npm-common/npmregistry");
let registry = npmregistry.NpmRegistry.FromServiceEndpoint('endpointId');

assert(registry.auth.match(BEARER_AUTH_REGEX), `Auth must contain _authToken. Auth is: (${registry.auth})`);
assert(registry.auth.match(AWLAYS_AUTH_REGEX), `Auth must contain always-auth. Auth is: (${registry.auth})`);

done();
});

it('does Bearer auth for onprem when service endpoint auth is Token and the endpoint is an IP addr', (done: MochaDone) => {
// Scenario: Onprem and user supplied an IP for the endpoint. We must assume that it is a 3rd party repo
// and, as such, will use bearer auth.
let mockTask = {
getVariable: (v) => {
if (v === 'System.TeamFoundationCollectionUri') {
return 'http://mytfsserver.example.com';
}
},
getEndpointAuthorization: (id, optional) => {
return { scheme: 'Token', parameters: { 'apitoken': 'AUTHTOKEN' } };
},
getEndpointUrl: (id, optional) => {
return 'http://10.10.10.10:8080/some/random/path';
}
};
mockery.registerMock('vsts-task-lib/task', mockTask);
const npmregistry = require("npm-common/npmregistry");
let registry = npmregistry.NpmRegistry.FromServiceEndpoint('endpointId');

assert(registry.auth.match(BEARER_AUTH_REGEX), `Auth must contain _authToken. Auth is: (${registry.auth})`);
assert(registry.auth.match(AWLAYS_AUTH_REGEX), `Auth must contain always-auth. Auth is: (${registry.auth})`);

done();
});

it('does Basic auth for onprem when service endpoint auth is Token and the TFS server and EP have the same IP', (done: MochaDone) => {
// Scenario: Onprem and user supplied an IP for the endpoint and the TeamFoundationCollectionUri is a _matching_ IP
let mockTask = {
getVariable: (v) => {
if (v === 'System.TeamFoundationCollectionUri') {
return 'http://10.10.10.10:8080/';
}
},
getEndpointAuthorization: (id, optional) => {
return { scheme: 'Token', parameters: { 'apitoken': 'AUTHTOKEN' } };
},
getEndpointUrl: (id, optional) => {
return 'http://10.10.10.10:8080/some/random/path';
}
};
mockery.registerMock('vsts-task-lib/task', mockTask);
const npmregistry = require("npm-common/npmregistry");
let registry = npmregistry.NpmRegistry.FromServiceEndpoint('endpointId');

assert(registry.auth.match(BASIC_AUTH_PAT_PASSWD_REGEX), `Auth must contain a password. Auth is: (${registry.auth})`);
assert(registry.auth.match(BASIC_AUTH_PAT_EML_REGEX), `Auth must contain a email. Auth is: (${registry.auth})`);
assert(registry.auth.match(BASIC_AUTH_PAT_USERNAME_REGEX), `Auth must contain a email. Auth is: (${registry.auth})`);
assert(registry.auth.match(AWLAYS_AUTH_REGEX), `Auth must contain always-auth. Auth is: (${registry.auth})`);

done();
});

it('does Basic auth for onprem when service endpoint auth is Token and the TFS server and EP have the same IP', (done: MochaDone) => {
// Scenario: Onprem and user supplied an IP for the endpoint and the TeamFoundationCollectionUri is a _matching_ IP
let mockTask = {
getVariable: (v) => {
if (v === 'System.TeamFoundationCollectionUri') {
return 'http://mytfsserver.example.com';
}
},
getEndpointAuthorization: (id, optional) => {
return { scheme: 'UsernamePassword', parameters: { 'username': 'USERNAME', 'password': 'PASSWORD' } };
},
getEndpointUrl: (id, optional) => {
return 'http://somepublicrepo.contoso.com:8080/some/random/path';
}
};
mockery.registerMock('vsts-task-lib/task', mockTask);
const npmregistry = require("npm-common/npmregistry");
let registry = npmregistry.NpmRegistry.FromServiceEndpoint('endpointId');

const BASIC_AUTH_PASSWD_REGEX = /\/\/.*\/:_password=PASSWORD.*/g;
assert(registry.auth.match(BASIC_AUTH_PAT_PASSWD_REGEX), `Auth must contain a password. Auth is: (${registry.auth})`);
const BASIC_AUTH_PAT_EML_REGEX = /\/\/.*\/:email=USERNAME.*/g;
assert(registry.auth.match(BASIC_AUTH_PAT_EML_REGEX), `Auth must contain a email. Auth is: (${registry.auth})`);
const BASIC_AUTH_PAT_USERNAME_REGEX = /\/\/.*\/:username=USERNAME.*/g;
assert(registry.auth.match(BASIC_AUTH_PAT_USERNAME_REGEX), `Auth must contain a email. Auth is: (${registry.auth})`);
assert(registry.auth.match(AWLAYS_AUTH_REGEX), `Auth must contain always-auth. Auth is: (${registry.auth})`);

done();
});

});
Loading

0 comments on commit 193a231

Please sign in to comment.