Skip to content

Commit

Permalink
Access: authorization flow
Browse files Browse the repository at this point in the history
  • Loading branch information
Dima Voytenko committed Dec 30, 2015
1 parent 3a28d82 commit 6a3bcfb
Show file tree
Hide file tree
Showing 6 changed files with 283 additions and 6 deletions.
9 changes: 9 additions & 0 deletions css/amp.css
Original file line number Diff line number Diff line change
Expand Up @@ -334,3 +334,12 @@ amp-analytics {
overflow: hidden !important;
visibility: hidden;
}


/**
* Minimal AMP Access CSS. This part has to be here so that the correct UI
* can be provided before AMP Access JS has been loaded.
*/
[amp-access][amp-access-off] {
display: none;
}
10 changes: 5 additions & 5 deletions examples/article-access.amp.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@
<meta name="viewport" content="width=device-width,minimum-scale=1,initial-scale=1">
<script id="amp-access" type="application/json">
{
"authorization": "http://localhost:8002/amp-authorization",
"pingback": "http://localhost:8002/amp-pingback",
"login": "http://localhost:8002/amp-login"
"authorization": "http://localhost:8002/amp-authorization.json?rid=READER_ID&url=CANONICAL_URL&_=RANDOM",
"pingback": "http://localhost:8002/amp-pingback?rid=READER_ID&url=CANONICAL_URL",
"login": "http://localhost:8002/amp-login?rid=READER_ID&url=CANONICAL_URL"
}
</script>
<link href='https://fonts.googleapis.com/css?family=Georgia|Open+Sans|Roboto' rel='stylesheet' type='text/css'>
Expand Down Expand Up @@ -179,11 +179,11 @@ <h1 itemprop="headline">Lorem Ipsum</h1>
</div>
</div>

<section class="login-section">
<section amp-access="NOT access" class="login-section">
<a on="tap:amp-access.login">Login to read more!</a>
</section>

<div class="article-body" itemprop="articleBody">
<div amp-access="access" class="article-body" itemprop="articleBody">
<p>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
Curabitur ullamcorper turpis vel commodo scelerisque. Phasellus
Expand Down
33 changes: 33 additions & 0 deletions extensions/amp-access/0.1/access-expr.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* Copyright 2015 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/


/**
* Evaluates access expression.
* @param {string} expr
* @param {!JSONObjectDef} data
* @return {boolean}
*/
export function evaluateAccessExpr(expr, data) {
// TODO(dvoytenko): the complete expression semantics
if (expr == 'access') {
return !!data.access;
}
if (expr == 'NOT access') {
return !data.access;
}
return false;
}
88 changes: 87 additions & 1 deletion extensions/amp-access/0.1/amp-access.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,15 @@

import {actionServiceFor} from '../../../src/action';
import {assertHttpsUrl} from '../../../src/url';
import {evaluateAccessExpr} from './access-expr';
import {getService} from '../../../src/service';
import {installStyles} from '../../../src/styles';
import {isExperimentOn} from '../../../src/experiments';
import {log} from '../../../src/log';
import {onDocumentReady} from '../../../src/document-state';
import {urlReplacementsFor} from '../../../src/url-replacements';
import {vsyncFor} from '../../../src/vsync';
import {xhrFor} from '../../../src/xhr';


/**
Expand Down Expand Up @@ -72,8 +77,17 @@ export class AccessService {
/** @const @private {!Element} */
this.accessElement_ = accessElement;

/** @const {!AccessConfigDef} */
/** @const @private {!AccessConfigDef} */
this.config_ = this.buildConfig_();

/** @const @private {!Vsync} */
this.vsync_ = vsyncFor(this.win);

/** @const @private {!Xhr} */
this.xhr_ = xhrFor(this.win);

/** @const @private {!UrlReplacements} */
this.urlReplacements_ = urlReplacementsFor(this.win);
}

/**
Expand Down Expand Up @@ -125,6 +139,78 @@ export class AccessService {
startInternal_() {
actionServiceFor(this.win).installActionHandler(
this.accessElement_, this.handleAction_.bind(this));

// Start authorization XHR immediately.
this.runAuthorization_();
}

/**
* @return {!Promise}
* @private
*/
runAuthorization_() {
log.fine(TAG, 'Start authorization via ', this.config_.authorization);
this.toggleTopClass_('amp-access-loading', true);

// TODO(dvoytenko): produce READER_ID and create the URL substition for it.
return this.urlReplacements_.expand(this.config_.authorization)
.then(url => {
log.fine(TAG, 'Authorization URL: ', url);
return this.xhr_.fetchJson(url, {credentials: 'include'});
})
.then(response => {
log.fine(TAG, 'Authorization response: ', response);
this.toggleTopClass_('amp-access-loading', false);
onDocumentReady(this.win.document, () => {
this.applyAuthorization_(response);
});
})
.catch(error => {
log.error(TAG, 'Authorization failed: ', error);
this.toggleTopClass_('amp-access-loading', false);
});
}

/**
* @param {!JSONObjectDef} response
* @private
*/
applyAuthorization_(response) {
const elements = this.win.document.querySelectorAll('[amp-access]');
for (let i = 0; i < elements.length; i++) {
this.applyAuthorizationToElement_(elements[i], response);
}
}

/**
* @param {!Element} element
* @param {!JSONObjectDef} response
* @private
*/
applyAuthorizationToElement_(element, response) {
const expr = element.getAttribute('amp-access');
const on = evaluateAccessExpr(expr, response);

// TODO(dvoytenko): support templates

this.vsync_.mutate(() => {
if (on) {
element.removeAttribute('amp-access-off');
} else {
element.setAttribute('amp-access-off', '');
}
});
}

/**
* @param {string} className
* @param {boolean} on
* @private
*/
toggleTopClass_(className, on) {
this.vsync_.mutate(() => {
this.win.document.documentElement.classList.toggle(className, on);
});
}

/**
Expand Down
47 changes: 47 additions & 0 deletions extensions/amp-access/0.1/test/test-access-expr.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* Copyright 2015 The AMP HTML Authors. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS-IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import {evaluateAccessExpr} from '../access-expr';


describe('evaluateAccessExpr', () => {

it('should evaluate simple boolean expressions', () => {
expect(evaluateAccessExpr('access', {})).to.be.false;

expect(evaluateAccessExpr('access', {access: true})).to.be.true;
expect(evaluateAccessExpr('access', {access: false})).to.be.false;

expect(evaluateAccessExpr('access', {access: 1})).to.be.true;
expect(evaluateAccessExpr('access', {access: 0})).to.be.false;

expect(evaluateAccessExpr('access', {access: '1'})).to.be.true;
expect(evaluateAccessExpr('access', {access: ''})).to.be.false;
});

it('should evaluate simple boolean NOT expressions', () => {
expect(evaluateAccessExpr('NOT access', {})).to.be.true;

expect(evaluateAccessExpr('NOT access', {access: true})).to.be.false;
expect(evaluateAccessExpr('NOT access', {access: false})).to.be.true;

expect(evaluateAccessExpr('NOT access', {access: 1})).to.be.false;
expect(evaluateAccessExpr('NOT access', {access: 0})).to.be.true;

expect(evaluateAccessExpr('NOT access', {access: '1'})).to.be.false;
expect(evaluateAccessExpr('NOT access', {access: ''})).to.be.true;
});
});
102 changes: 102 additions & 0 deletions extensions/amp-access/0.1/test/test-amp-access.js
Original file line number Diff line number Diff line change
Expand Up @@ -144,3 +144,105 @@ describe('AccessService', () => {
expect(service.startInternal_.callCount).to.equal(1);
});
});


describe('AccessService authorization', () => {

let sandbox;
let configElement, elementOn, elementOff;
let vsyncMutates;
let urlReplacementsMock;
let xhrMock;

beforeEach(() => {
sandbox = sinon.sandbox.create();

configElement = document.createElement('script');
configElement.setAttribute('id', 'amp-access');
configElement.setAttribute('type', 'application/json');
configElement.textContent = JSON.stringify({
'authorization': 'https://acme.com/a?rid=READER_ID',
'pingback': 'https://acme.com/p?rid=READER_ID',
'login': 'https://acme.com/l?rid=READER_ID'
});
document.body.appendChild(configElement);

elementOn = document.createElement('div');
elementOn.setAttribute('amp-access', 'access');
document.body.appendChild(elementOn);

elementOff = document.createElement('div');
elementOff.setAttribute('amp-access', 'NOT access');
document.body.appendChild(elementOff);

service = new AccessService(window);
service.isExperimentOn_ = true;

vsyncMutates = [];
service.vsync_ = {mutate: callback => vsyncMutates.push(callback)};
urlReplacementsMock = sandbox.mock(service.urlReplacements_);
xhrMock = sandbox.mock(service.xhr_);
});

afterEach(() => {
if (configElement.parentElement) {
configElement.parentElement.removeChild(configElement);
}
if (elementOn.parentElement) {
elementOn.parentElement.removeChild(elementOn);
}
if (elementOff.parentElement) {
elementOff.parentElement.removeChild(elementOff);
}
sandbox.restore();
sandbox = null;
});

it('should run authorization flow', () => {
urlReplacementsMock.expects('expand')
.withExactArgs('https://acme.com/a?rid=READER_ID')
.returns(Promise.resolve('https://acme.com/a?rid=reader1'))
.once();
xhrMock.expects('fetchJson')
.withExactArgs('https://acme.com/a?rid=reader1',
{credentials: 'include'})
.returns(Promise.resolve({access: true}))
.once();
return service.runAuthorization_().then(() => {
expect(vsyncMutates).to.have.length.greaterThan(2);
vsyncMutates.shift()();
expect(document.documentElement).to.have.class('amp-access-loading');

while (vsyncMutates.length > 0) {
vsyncMutates.shift()();
}
expect(document.documentElement).not.to.have.class('amp-access-loading');
expect(elementOn).not.to.have.attribute('amp-access-off');
expect(elementOff).to.have.attribute('amp-access-off');
});
});

it('should recover from authorization failure', () => {
urlReplacementsMock.expects('expand')
.withExactArgs('https://acme.com/a?rid=READER_ID')
.returns(Promise.resolve('https://acme.com/a?rid=reader1'))
.once();
xhrMock.expects('fetchJson')
.withExactArgs('https://acme.com/a?rid=reader1',
{credentials: 'include'})
.returns(Promise.reject())
.once();
return service.runAuthorization_().then(() => {
expect(vsyncMutates).to.have.length.greaterThan(1);
vsyncMutates.shift()();
expect(document.documentElement).to.have.class('amp-access-loading');

while (vsyncMutates.length > 0) {
vsyncMutates.shift()();
}
expect(document.documentElement).not.to.have.class('amp-access-loading');
expect(elementOn).not.to.have.attribute('amp-access-off');
expect(elementOff).not.to.have.attribute('amp-access-off');
});
});
});

0 comments on commit 6a3bcfb

Please sign in to comment.