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

Add support for token based auth to services other than VSTS #6440

Merged
merged 1 commit into from
Feb 21, 2018
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
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);
Copy link
Contributor

Choose a reason for hiding this comment

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

keeping this console.log is important, it basically says "this string is a password, obfuscate it in any logging"

Copy link

Choose a reason for hiding this comment

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

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Fixed


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