Skip to content

Commit

Permalink
Migrate accounts+capabilities to new JMAP authentication spec
Browse files Browse the repository at this point in the history
According to the recent updates to the JMAP spec, account data is now loaded from the authentication response. Therefore the AuthAccess model was significantly changed to serve as a container for authentication data, account information and server capabilities.

This change requires authentication responses to comply with the latest spec as the model requires certain new properties to be present.

Added: jmap.ServerCapablities
Removed: jmap.AccountCapabilities

The namespace URIs used for the various capabilites are defined in jmap.Constants but not yet final.

see links in issue linagora#58
  • Loading branch information
thomascube committed Oct 3, 2017
1 parent 3812ff4 commit b6eb18b
Show file tree
Hide file tree
Showing 16 changed files with 495 additions and 222 deletions.
4 changes: 2 additions & 2 deletions lib/API.js
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@
* @property AuthContinuation {AuthContinuation} The {@link AuthContinuation} class
* @property Constants {Constants} The {@link module:Constants|Constants} object
* @property Attachment {Attachment} The {@link Attachment} class
* @property AccountCapabilities {AccountCapabilities} The {@link AccountCapabilities} class
* @property Capabilities {Capabilities} The {@link Capabilities} class
* @property MailCapabilities {MailCapabilities} The {@link MailCapabilities} class
* @property ServerCapabilities {ServerCapabilities} The {@link ServerCapabilities} class
* @property VacationResponse {VacationResponse} The {@link VacationResponse} class
* @property TransportError {TransportError} The {@link TransportError} class
* @property JmapError {JmapError} The {@link JmapError} class
Expand Down Expand Up @@ -70,9 +70,9 @@ export default {
AuthMethod: require('./models/AuthMethod'),
Constants: require('./utils/Constants'),
Attachment: require('./models/Attachment'),
AccountCapabilities: require('./models/AccountCapabilities'),
Capabilities: require('./models/Capabilities'),
MailCapabilities: require('./models/MailCapabilities'),
ServerCapabilities: require('./models/ServerCapabilities'),
VacationResponse: require('./models/VacationResponse'),
TransportError: require('./errors/TransportError'),
JmapError: require('./errors/JmapError')
Expand Down
31 changes: 27 additions & 4 deletions lib/client/Client.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,16 +103,22 @@ export default class Client {
* <br />
* The individual properties of the AuthAccess object will be copied into client properties.
*
* @param access {AuthAccess} The response object from an authenticate process.
* @param access {AuthAccess|Object} The response object from an authenticate process.
*
* @return {Client} This Client instance.
*/
withAuthAccess(access) {
Utils.assertRequiredParameterHasType(access, 'access', AuthAccess);
Utils.assertRequiredParameterIsObject(access, 'access');

// create an instance of AuthAccess if plain object is given
if (!(access instanceof AuthAccess)) {
access = new AuthAccess(this, access);
}

this.authAccess = access;
this.authScheme = 'X-JMAP';
this.authToken = access.accessToken;
['username', 'apiUrl', 'eventSourceUrl', 'uploadUrl', 'downloadUrl', 'versions', 'extensions'].forEach((property) => {
['username', 'signingId', 'signingKey', 'apiUrl', 'eventSourceUrl', 'uploadUrl', 'downloadUrl', 'serverCapabilities', 'mailCapabilities'].forEach((property) => {
this[property] = access[property];
});

Expand Down Expand Up @@ -182,7 +188,7 @@ export default class Client {
.then(resp => this._authenticateResponse(resp, continuationCallback));
} else if (data.accessToken && data.loginId === undefined) {
// got auth access response
return new AuthAccess(data);
return new AuthAccess(this, data);
} else {
// got unknown response data
throw new Error('Unexpected response on authorization request');
Expand Down Expand Up @@ -250,6 +256,23 @@ export default class Client {
* @see http://jmap.io/spec.html#getaccounts
*/
getAccounts(options) {
// resolve with accounts list from AuthAccess
if (this.authAccess instanceof AuthAccess) {
return this.promiseProvider.newPromise(function(resolve, reject) {
let accounts = [];

// equivalent to Object.values()
for (let id in this.authAccess.accounts) {
if (this.authAccess.accounts.hasOwnProperty(id)) {
accounts.push(this.authAccess.accounts[id]);
}
}

resolve(accounts);
}.bind(this));
}

// fallback to deprecated getAccounts request
return this._jmapRequest('getAccounts', options);
}

Expand Down
37 changes: 23 additions & 14 deletions lib/models/Account.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@

import Model from './Model.js';
import Utils from '../utils/Utils.js';
import Capabilities from './Capabilities';
import MailCapabilities from './MailCapabilities';
import AccountCapabilities from './AccountCapabilities';
import JSONBuilder from '../utils/JSONBuilder.js';

export default class Account extends Model {
/**
Expand All @@ -18,10 +16,9 @@ export default class Account extends Model {
* @param [opts] {Object} The optional properties of this _Account_.
* @param [opts.name=''] {String} The name of this _Account_.
* @param [opts.isPrimary=false] {Boolean} Whether this _Account_ is the primary email account.
* @param [opts.capabilities=null] {AccountCapabilities} An object describing general capabilities of this server.
* @param [opts.mail=null] {MailCapabilities} If null, this account does not support mail. Otherwise, the {MailCapabilities} of the server.
* @param [opts.contacts=null] {Capabilities} If null, this account does not support contacts. Otherwise, the contacts {Capabilities} of the server.
* @param [opts.calendars=null] {Capabilities} If null, this account does not support calendars. Otherwise, the calendars {Capabilities} of the server.
* @param [opts.isReadOnly=false] {Boolean} Whether the entire _Account_ is read-only
* @param [opts.hasDataFor=[]] {String[]} A list of the data profiles available in this account
server.
*
* @see Model
*/
Expand All @@ -35,10 +32,8 @@ export default class Account extends Model {
this.id = id;
this.name = opts.name || '';
this.isPrimary = opts.isPrimary || false;
this.capabilities = Utils._nullOrNewInstance(opts.capabilities, AccountCapabilities);
this.mail = Utils._nullOrNewInstance(opts.mail, MailCapabilities);
this.contacts = Utils._nullOrNewInstance(opts.contacts, Capabilities);
this.calendars = Utils._nullOrNewInstance(opts.calendars, Capabilities);
this.isReadOnly = opts.isReadOnly || false;
this.hasDataFor = opts.hasDataFor || [];
}

/**
Expand All @@ -47,7 +42,7 @@ export default class Account extends Model {
* @returns {Boolean} _true_ if and only if this `Account` has mail enabled, _false_ otherwise.
*/
hasMail() {
return this.mail !== null;
return this.hasDataFor.indexOf('mail') >= 0;
}

/**
Expand All @@ -56,7 +51,7 @@ export default class Account extends Model {
* @returns {Boolean} _true_ if and only if this `Account` has calendars enabled, _false_ otherwise.
*/
hasCalendars() {
return this.calendars !== null;
return this.hasDataFor.indexOf('calendars') >= 0;
}

/**
Expand All @@ -65,7 +60,7 @@ export default class Account extends Model {
* @returns {Boolean} _true_ if and only if this `Account` has contacts enabled, _false_ otherwise.
*/
hasContacts() {
return this.contacts !== null;
return this.hasDataFor.indexOf('contacts') >= 0;
}

/**
Expand All @@ -86,6 +81,20 @@ export default class Account extends Model {
return this._jmap.getMailboxes(options);
}

/**
* Creates a JSON representation from this {@link Account}.
*
* @return JSON object with only owned properties and non-null default values.
*/
toJSONObject() {
return new JSONBuilder()
.append('name', this.name)
.append('isPrimary', this.isPrimary)
.append('isReadOnly', this.isReadOnly)
.append('hasDataFor', this.hasDataFor)
.build();
}

/**
* Creates an _Account_ from its JSON representation.
*
Expand Down
18 changes: 0 additions & 18 deletions lib/models/AccountCapabilities.js

This file was deleted.

48 changes: 42 additions & 6 deletions lib/models/AuthAccess.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,65 @@
'use strict';

import Model from './Model.js';
import Utils from '../utils/Utils.js';
import Constants from '../utils/Constants.js';
import Account from './Account.js';
import ServerCapabilities from './ServerCapabilities.js';
import MailCapabilities from './MailCapabilities.js';
import JSONBuilder from '../utils/JSONBuilder.js';

export default class AuthAccess {
export default class AuthAccess extends Model {
/**
* This class represents a JMAP [Auth Access Response]{@link http://jmap.io/spec.html#authentication}.
* This class represents a JMAP [Auth Access Response]{@link http://jmap.io/spec-core.html#201-authentication-is-complete-access-token-created}.
*
* @constructor
*
* @param jmap {Client} The {@link Client} instance that created this _AuthAccess_.
* @param payload {Object} The server response of an auth access request.
*/
constructor(jmap, payload) {
super(jmap);

constructor(payload) {
Utils.assertRequiredParameterIsPresent(payload, 'payload');
['username', 'accessToken', 'apiUrl', 'eventSourceUrl', 'uploadUrl', 'downloadUrl'].forEach((property) => {
['username', 'accessToken', 'signingId', 'signingKey', 'apiUrl', 'eventSourceUrl', 'uploadUrl', 'downloadUrl', 'accounts', 'capabilities'].forEach((property) => {
Utils.assertRequiredParameterIsPresent(payload[property], property);
});

this.username = payload.username;
this.versions = payload.versions || [];
this.extensions = payload.extensions || {};
this.accessToken = payload.accessToken;
this.signingId = payload.signingId;
this.signingKey = payload.signingKey;
this.apiUrl = payload.apiUrl;
this.eventSourceUrl = payload.eventSourceUrl;
this.uploadUrl = payload.uploadUrl;
this.downloadUrl = payload.downloadUrl;
this.capabilities = payload.capabilities;
this.serverCapabilities = new ServerCapabilities(this.capabilities[Constants.CORE_CAPABILITIES_URI] || {});
this.mailCapabilities = new MailCapabilities(this.capabilities[Constants.MAIL_CAPABILITIES_URI] || {});

this.accounts = {};
for (var accountId in payload.accounts) {
this.accounts[accountId] = Account.fromJSONObject(jmap, Object.assign({ id: accountId }, payload.accounts[accountId]));
}
}

/**
* Creates a JSON representation from this {@link AuthAccess}.
*
* @return JSON object with only owned properties and non-null default values.
*/
toJSONObject() {
return new JSONBuilder()
.append('username', this.username)
.append('accessToken', this.accessToken)
.append('signingId', this.signingId)
.append('signingKey', this.signingKey)
.append('apiUrl', this.apiUrl)
.append('eventSourceUrl', this.eventSourceUrl)
.append('uploadUrl', this.uploadUrl)
.append('downloadUrl', this.downloadUrl)
.appendObject('accounts', this.accounts)
.appendObject('capabilities', this.capabilities)
.build();
}
}
16 changes: 10 additions & 6 deletions lib/models/Capabilities.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,20 @@
'use strict';

import Utils from '../utils/Utils.js';

export default class Capabilities {
/**
* This class represents an abstract JMAP "capabilities" object. See {@link http://jmap.io/spec.html#accounts*}.<br />
* It is subclassed in more specific xxxCapabilities classes.
* This class represents an generic JMAP "capabilities" object. See {@link http://jmap.io/spec-core.html#getting-an-access-token*}.<br />
*
* @param [opts] {Object} The optional properties of this _AccountCapabilities_.
* @param [opts.isReadOnly=false] {Boolean} Whether the feature denoted by this _Capabilities_ instance is read-only.
* @param namespace {String} The namespace/identifier of the capabilities.
* @param [opts] {Object} The optional properties of this _Capabilities_.
*/
constructor(opts) {
constructor(namespace, opts) {
Utils.assertRequiredParameterIsPresent(namespace, 'namespace');

opts = opts || {};

this.isReadOnly = !!opts.isReadOnly;
this.ns = namespace;
Object.assign(this, opts);
}
}
13 changes: 9 additions & 4 deletions lib/models/MailCapabilities.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
'use strict';

import Capabilities from './Capabilities';
import Constants from '../utils/Constants';

export default class MailCapabilities extends Capabilities {
/**
Expand All @@ -11,19 +12,23 @@ export default class MailCapabilities extends Capabilities {
* @extends Capabilities
*
* @param [opts] {Object} The optional properties of this _MailCapabilities_.
* @param [opts.maxMailboxesPerMessage=null] {Number} The maximum number of mailboxes that can be can assigned to a single message.
* @param [opts.maxSizeMessageAttachments=0] {Number} The maximum total size of attachments, in bytes, allowed for messages.
* @param [opts.canDelaySend=false] {Boolean} Whether the server supports inserting a message into the outbox to be sent later.
* @param [opts.messageListSortOptions=0] {String[]} A list of all the message properties the server supports for sorting by.
* @param [opts.maxDelayedSend=0] {Number} The number in seconds of the maximum delay the server supports in sending. 0 if delayed send is not supported.
* @param [opts.messageListSortOptions=[]] {String[]} A list of all the message properties the server supports for sorting by.
* @param [opts.submissionExtensions={} {String[String[]]} Known safe-to-expose EHLO capabilities of the submission server.
*
* @see Capabilities
*/
constructor(opts) {
opts = opts || {};

super(opts);
super(Constants.MAIL_CAPABILITIES_URI);

this.maxMailboxesPerMessage = opts.maxMailboxesPerMessage || null;
this.maxSizeMessageAttachments = opts.maxSizeMessageAttachments || 0;
this.canDelaySend = !!opts.canDelaySend;
this.maxDelayedSend = opts.maxDelayedSend || 0;
this.messageListSortOptions = opts.messageListSortOptions || [];
this.submissionExtensions = opts.submissionExtensions || {};
}
}
30 changes: 30 additions & 0 deletions lib/models/ServerCapabilities.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
'use strict';

export default class ServerCapabilities {
/**
* This class represents a JMAP [ServerCapabilities]{@link http://jmap.io/spec-core.html#201-authentication-is-complete-access-token-created}.<br />
* An _ServerCapabilities_ object describes general capabilities of a JMAP server.
*
* @constructor
*
* @param [opts] {Object} The optional properties of this _ServerCapabilities_.
* @param [opts.maxSizeUpload=0] {Number} The maximum file size, in bytes, that the server will accept for a single file upload.
* @param [opts.maxSizeRequest=0] {Number} The maximum size, in bytes, that the server will accept for a single request to the API endpoint.
* @param [opts.maxConcurrentUpload=1] {Number} The maximum number of concurrent requests the server will accept to the upload endpoint.
* @param [opts.maxConcurrentRequests=1] {Number} The maximum number of concurrent requests the server will accept to the API endpoint.
* @param [opts.maxCallsInRequest=1] {Number} The maximum number of method calls the server will accept in a single request to the API endpoint.
* @param [opts.maxObjectsInGet=0] {Number} The maximum number of objects that the client may request in a single getXXX type method call.
* @param [opts.maxObjectsInSet=0] {Number} The maximum number of objects the client may send to create, update or destroy in a single setXXX type method call.
*/
constructor(opts) {
opts = opts || {};

this.maxSizeUpload = opts.maxSizeUpload || 0;
this.maxSizeRequest = opts.maxSizeRequest || 0;
this.maxConcurrentUpload = opts.maxConcurrentUpload || 1;
this.maxConcurrentRequests = opts.maxConcurrentRequests || 1;
this.maxCallsInRequest = opts.maxCallsInRequest || 1;
this.maxObjectsInGet = opts.maxObjectsInGet || 0;
this.maxObjectsInSet = opts.maxObjectsInSet || 0;
}
}
4 changes: 3 additions & 1 deletion lib/utils/Constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,7 @@ export default {
VERSION: '__VERSION__',
CLIENT_NAME: 'jmap-client (https://github.com/linagora/jmap-client)',
SUCCESS_RESPONSE_CODES: [200, 201],
ERROR: 'error'
ERROR: 'error',
CORE_CAPABILITIES_URI: 'http://jmap.io/spec-core.html',
MAIL_CAPABILITIES_URI: 'http://jmap.io/spec-mail.html'
};
Loading

0 comments on commit b6eb18b

Please sign in to comment.