New: requireAnyUserRoles and requireAllUserRoles for Parse Cloud Validator (#7097)

* new: requireUserRole for Parse Cloud Validator

* change to requireUserRoles

* Update CHANGELOG.md

* revoke triggers

* Update triggers.js

* Update ParseLiveQueryServer.js

* Update ParseLiveQueryServer.js

* create requireUserRoles

* rename to requireAny and requireAll

* allow for a function
This commit is contained in:
dblythy
2021-02-13 09:01:38 +11:00
committed by GitHub
parent 7224cde023
commit 3d76643286
6 changed files with 376 additions and 211 deletions

View File

@@ -17,6 +17,8 @@ ___
- IMPROVE: Optimize queries on classes with pointer permissions. [#7061](https://github.com/parse-community/parse-server/pull/7061). Thanks to [Pedro Diaz](https://github.com/pdiaz)
- IMPROVE: Parse Server will from now on be continuously tested against all relevant Postgres versions (minor versions). Added Postgres compatibility table to Parse Server docs. [#7176](https://github.com/parse-community/parse-server/pull/7176). Thanks to [Corey Baker](https://github.com/cbaker6).
- FIX: request.context for afterFind triggers. [#7078](https://github.com/parse-community/parse-server/pull/7078). Thanks to [dblythy](https://github.com/dblythy)
- NEW: `requireAnyUserRoles` and `requireAllUserRoles` for Parse Cloud validator. [#7097](https://github.com/parse-community/parse-server/pull/7097). Thanks to [dblythy](https://github.com/dblythy)
- NEW: Added convenience method Parse.Cloud.sendEmail(...) to send email via email adapter in Cloud Code. [#7089](https://github.com/parse-community/parse-server/pull/7089). Thanks to [dblythy](https://github.com/dblythy)
- FIX: Winston Logger interpolating stdout to console [#7114](https://github.com/parse-community/parse-server/pull/7114). Thanks to [dplewis](https://github.com/dplewis)
### 4.5.0

View File

@@ -878,6 +878,150 @@ describe('cloud validator', () => {
});
});
it('basic validator requireAnyUserRoles', async function (done) {
Parse.Cloud.define(
'cloudFunction',
() => {
return true;
},
{
requireUser: true,
requireAnyUserRoles: ['Admin'],
}
);
const user = await Parse.User.signUp('testuser', 'p@ssword');
try {
await Parse.Cloud.run('cloudFunction');
fail('cloud validator should have failed.');
} catch (e) {
expect(e.message).toBe('Validation failed. User does not match the required roles.');
}
const roleACL = new Parse.ACL();
roleACL.setPublicReadAccess(true);
const role = new Parse.Role('Admin', roleACL);
role.getUsers().add(user);
await role.save({ useMasterKey: true });
await Parse.Cloud.run('cloudFunction');
done();
});
it('basic validator requireAllUserRoles', async function (done) {
Parse.Cloud.define(
'cloudFunction',
() => {
return true;
},
{
requireUser: true,
requireAllUserRoles: ['Admin', 'Admin2'],
}
);
const user = await Parse.User.signUp('testuser', 'p@ssword');
try {
await Parse.Cloud.run('cloudFunction');
fail('cloud validator should have failed.');
} catch (e) {
expect(e.message).toBe('Validation failed. User does not match all the required roles.');
}
const roleACL = new Parse.ACL();
roleACL.setPublicReadAccess(true);
const role = new Parse.Role('Admin', roleACL);
role.getUsers().add(user);
const role2 = new Parse.Role('Admin2', roleACL);
role2.getUsers().add(user);
await Promise.all([role.save({ useMasterKey: true }), role2.save({ useMasterKey: true })]);
await Parse.Cloud.run('cloudFunction');
done();
});
it('allow requireAnyUserRoles to be a function', async function (done) {
Parse.Cloud.define(
'cloudFunction',
() => {
return true;
},
{
requireUser: true,
requireAnyUserRoles: () => {
return ['Admin Func'];
},
}
);
const user = await Parse.User.signUp('testuser', 'p@ssword');
try {
await Parse.Cloud.run('cloudFunction');
fail('cloud validator should have failed.');
} catch (e) {
expect(e.message).toBe('Validation failed. User does not match the required roles.');
}
const roleACL = new Parse.ACL();
roleACL.setPublicReadAccess(true);
const role = new Parse.Role('Admin Func', roleACL);
role.getUsers().add(user);
await role.save({ useMasterKey: true });
await Parse.Cloud.run('cloudFunction');
done();
});
it('allow requireAllUserRoles to be a function', async function (done) {
Parse.Cloud.define(
'cloudFunction',
() => {
return true;
},
{
requireUser: true,
requireAllUserRoles: () => {
return ['AdminA', 'AdminB'];
},
}
);
const user = await Parse.User.signUp('testuser', 'p@ssword');
try {
await Parse.Cloud.run('cloudFunction');
fail('cloud validator should have failed.');
} catch (e) {
expect(e.message).toBe('Validation failed. User does not match all the required roles.');
}
const roleACL = new Parse.ACL();
roleACL.setPublicReadAccess(true);
const role = new Parse.Role('AdminA', roleACL);
role.getUsers().add(user);
const role2 = new Parse.Role('AdminB', roleACL);
role2.getUsers().add(user);
await Promise.all([role.save({ useMasterKey: true }), role2.save({ useMasterKey: true })]);
await Parse.Cloud.run('cloudFunction');
done();
});
it('basic requireAllUserRoles but no user', async function (done) {
Parse.Cloud.define(
'cloudFunction',
() => {
return true;
},
{
requireAllUserRoles: ['Admin'],
}
);
try {
await Parse.Cloud.run('cloudFunction');
fail('cloud validator should have failed.');
} catch (e) {
expect(e.message).toBe('Validation failed. Please login to continue.');
}
const user = await Parse.User.signUp('testuser', 'p@ssword');
const roleACL = new Parse.ACL();
roleACL.setPublicReadAccess(true);
const role = new Parse.Role('Admin', roleACL);
role.getUsers().add(user);
await role.save({ useMasterKey: true });
await Parse.Cloud.run('cloudFunction');
done();
});
it('basic beforeSave requireMaster', function (done) {
Parse.Cloud.beforeSave('BeforeSaveFail', () => {}, {
requireMaster: true,

View File

@@ -10,12 +10,7 @@ import { ParsePubSub } from './ParsePubSub';
import SchemaController from '../Controllers/SchemaController';
import _ from 'lodash';
import { v4 as uuidv4 } from 'uuid';
import {
runLiveQueryEventHandlers,
maybeRunConnectTrigger,
maybeRunSubscribeTrigger,
maybeRunAfterEventTrigger,
} from '../triggers';
import { runLiveQueryEventHandlers, getTrigger, runTrigger } from '../triggers';
import { getAuthForSessionToken, Auth } from '../Auth';
import { getCacheController } from '../Controllers';
import LRU from 'lru-cache';
@@ -121,7 +116,7 @@ class ParseLiveQueryServer {
// Message is the JSON object from publisher after inflated. Message.currentParseObject is the ParseObject after changes.
// Message.originalParseObject is the original ParseObject.
_onAfterDelete(message: any): void {
async _onAfterDelete(message: any): void {
logger.verbose(Parse.applicationId + 'afterDelete is triggered');
let deletedParseObject = message.currentParseObject.toJSON();
@@ -135,6 +130,7 @@ class ParseLiveQueryServer {
logger.debug('Can not find subscriptions under this class ' + className);
return;
}
for (const subscription of classSubscriptions.values()) {
const isSubscriptionMatched = this._matchesSubscription(deletedParseObject, subscription);
if (!isSubscriptionMatched) {
@@ -145,63 +141,71 @@ class ParseLiveQueryServer {
if (typeof client === 'undefined') {
continue;
}
for (const requestId of requestIds) {
requestIds.forEach(async requestId => {
const acl = message.currentParseObject.getACL();
// Check CLP
const op = this._getCLPOperation(subscription.query);
let res = {};
this._matchesCLP(classLevelPermissions, message.currentParseObject, client, requestId, op)
.then(() => {
// Check ACL
return this._matchesACL(acl, client, requestId);
})
.then(isMatched => {
if (!isMatched) {
return null;
try {
await this._matchesCLP(
classLevelPermissions,
message.currentParseObject,
client,
requestId,
op
);
const isMatched = await this._matchesACL(acl, client, requestId);
if (!isMatched) {
return null;
}
res = {
event: 'delete',
sessionToken: client.sessionToken,
object: deletedParseObject,
clients: this.clients.size,
subscriptions: this.subscriptions.size,
useMasterKey: client.hasMasterKey,
installationId: client.installationId,
sendEvent: true,
};
const trigger = getTrigger(className, 'afterEvent', Parse.applicationId);
if (trigger) {
const auth = await this.getAuthForSessionToken(res.sessionToken);
res.user = auth.user;
if (res.object) {
res.object = Parse.Object.fromJSON(res.object);
}
res = {
event: 'delete',
sessionToken: client.sessionToken,
object: deletedParseObject,
clients: this.clients.size,
subscriptions: this.subscriptions.size,
useMasterKey: client.hasMasterKey,
installationId: client.installationId,
sendEvent: true,
};
return maybeRunAfterEventTrigger('afterEvent', className, res);
})
.then(() => {
if (!res.sendEvent) {
return;
}
if (res.object && typeof res.object.toJSON === 'function') {
deletedParseObject = res.object.toJSON();
deletedParseObject.className = className;
}
client.pushDelete(requestId, deletedParseObject);
})
.catch(error => {
Client.pushError(
client.parseWebSocket,
error.code || 141,
error.message || error,
false,
requestId
);
logger.error(
`Failed running afterLiveQueryEvent on class ${className} for event ${res.event} with session ${res.sessionToken} with:\n Error: ` +
JSON.stringify(error)
);
});
}
await runTrigger(trigger, `afterEvent.${className}`, res, auth);
}
if (!res.sendEvent) {
return;
}
if (res.object && typeof res.object.toJSON === 'function') {
deletedParseObject = res.object.toJSON();
deletedParseObject.className = className;
}
client.pushDelete(requestId, deletedParseObject);
} catch (error) {
Client.pushError(
client.parseWebSocket,
error.code || 141,
error.message || error,
false,
requestId
);
logger.error(
`Failed running afterLiveQueryEvent on class ${className} for event ${res.event} with session ${res.sessionToken} with:\n Error: ` +
JSON.stringify(error)
);
}
});
}
}
}
// Message is the JSON object from publisher after inflated. Message.currentParseObject is the ParseObject after changes.
// Message.originalParseObject is the original ParseObject.
_onAfterSave(message: any): void {
async _onAfterSave(message: any): void {
logger.verbose(Parse.applicationId + 'afterSave is triggered');
let originalParseObject = null;
@@ -233,7 +237,7 @@ class ParseLiveQueryServer {
if (typeof client === 'undefined') {
continue;
}
for (const requestId of requestIds) {
requestIds.forEach(async requestId => {
// Set orignal ParseObject ACL checking promise, if the object does not match
// subscription, we do not need to check ACL
let originalACLCheckingPromise;
@@ -256,86 +260,99 @@ class ParseLiveQueryServer {
const currentACL = message.currentParseObject.getACL();
currentACLCheckingPromise = this._matchesACL(currentACL, client, requestId);
}
const op = this._getCLPOperation(subscription.query);
this._matchesCLP(classLevelPermissions, message.currentParseObject, client, requestId, op)
.then(() => {
return Promise.all([originalACLCheckingPromise, currentACLCheckingPromise]);
})
.then(([isOriginalMatched, isCurrentMatched]) => {
logger.verbose(
'Original %j | Current %j | Match: %s, %s, %s, %s | Query: %s',
originalParseObject,
currentParseObject,
isOriginalSubscriptionMatched,
isCurrentSubscriptionMatched,
isOriginalMatched,
isCurrentMatched,
subscription.hash
);
// Decide event type
let type;
if (isOriginalMatched && isCurrentMatched) {
type = 'update';
} else if (isOriginalMatched && !isCurrentMatched) {
type = 'leave';
} else if (!isOriginalMatched && isCurrentMatched) {
if (originalParseObject) {
type = 'enter';
} else {
type = 'create';
}
} else {
return null;
}
message.event = type;
res = {
event: type,
sessionToken: client.sessionToken,
object: currentParseObject,
original: originalParseObject,
clients: this.clients.size,
subscriptions: this.subscriptions.size,
useMasterKey: client.hasMasterKey,
installationId: client.installationId,
sendEvent: true,
};
return maybeRunAfterEventTrigger('afterEvent', className, res);
})
.then(
() => {
if (!res.sendEvent) {
return;
}
if (res.object && typeof res.object.toJSON === 'function') {
currentParseObject = res.object.toJSON();
currentParseObject.className = res.object.className || className;
}
if (res.original && typeof res.original.toJSON === 'function') {
originalParseObject = res.original.toJSON();
originalParseObject.className = res.original.className || className;
}
const functionName =
'push' + message.event.charAt(0).toUpperCase() + message.event.slice(1);
if (client[functionName]) {
client[functionName](requestId, currentParseObject, originalParseObject);
}
},
error => {
Client.pushError(
client.parseWebSocket,
error.code || 141,
error.message || error,
false,
requestId
);
logger.error(
`Failed running afterLiveQueryEvent on class ${className} for event ${res.event} with session ${res.sessionToken} with:\n Error: ` +
JSON.stringify(error)
);
}
try {
const op = this._getCLPOperation(subscription.query);
await this._matchesCLP(
classLevelPermissions,
message.currentParseObject,
client,
requestId,
op
);
}
const [isOriginalMatched, isCurrentMatched] = await Promise.all([
originalACLCheckingPromise,
currentACLCheckingPromise,
]);
logger.verbose(
'Original %j | Current %j | Match: %s, %s, %s, %s | Query: %s',
originalParseObject,
currentParseObject,
isOriginalSubscriptionMatched,
isCurrentSubscriptionMatched,
isOriginalMatched,
isCurrentMatched,
subscription.hash
);
// Decide event type
let type;
if (isOriginalMatched && isCurrentMatched) {
type = 'update';
} else if (isOriginalMatched && !isCurrentMatched) {
type = 'leave';
} else if (!isOriginalMatched && isCurrentMatched) {
if (originalParseObject) {
type = 'enter';
} else {
type = 'create';
}
} else {
return null;
}
message.event = type;
res = {
event: type,
sessionToken: client.sessionToken,
object: currentParseObject,
original: originalParseObject,
clients: this.clients.size,
subscriptions: this.subscriptions.size,
useMasterKey: client.hasMasterKey,
installationId: client.installationId,
sendEvent: true,
};
const trigger = getTrigger(className, 'afterEvent', Parse.applicationId);
if (trigger) {
if (res.object) {
res.object = Parse.Object.fromJSON(res.object);
}
if (res.original) {
res.original = Parse.Object.fromJSON(res.original);
}
const auth = await this.getAuthForSessionToken(res.sessionToken);
res.user = auth.user;
await runTrigger(trigger, `afterEvent.${className}`, res, auth);
}
if (!res.sendEvent) {
return;
}
if (res.object && typeof res.object.toJSON === 'function') {
currentParseObject = res.object.toJSON();
currentParseObject.className = res.object.className || className;
}
if (res.original && typeof res.original.toJSON === 'function') {
originalParseObject = res.original.toJSON();
originalParseObject.className = res.original.className || className;
}
const functionName =
'push' + message.event.charAt(0).toUpperCase() + message.event.slice(1);
if (client[functionName]) {
client[functionName](requestId, currentParseObject, originalParseObject);
}
} catch (error) {
Client.pushError(
client.parseWebSocket,
error.code || 141,
error.message || error,
false,
requestId
);
logger.error(
`Failed running afterLiveQueryEvent on class ${className} for event ${res.event} with session ${res.sessionToken} with:\n Error: ` +
JSON.stringify(error)
);
}
});
}
}
}
@@ -614,7 +631,12 @@ class ParseLiveQueryServer {
useMasterKey: client.hasMasterKey,
installationId: request.installationId,
};
await maybeRunConnectTrigger('beforeConnect', req);
const trigger = getTrigger('@Connect', 'beforeConnect', Parse.applicationId);
if (trigger) {
const auth = await this.getAuthForSessionToken(req.sessionToken);
req.user = auth.user;
await runTrigger(trigger, `beforeConnect.@Connect`, req, auth);
}
parseWebsocket.clientId = clientId;
this.clients.set(parseWebsocket.clientId, client);
logger.info(`Create new client: ${parseWebsocket.clientId}`);
@@ -668,7 +690,22 @@ class ParseLiveQueryServer {
const client = this.clients.get(parseWebsocket.clientId);
const className = request.query.className;
try {
await maybeRunSubscribeTrigger('beforeSubscribe', className, request);
const trigger = getTrigger(className, 'beforeSubscribe', Parse.applicationId);
if (trigger) {
const auth = await this.getAuthForSessionToken(request.sessionToken);
request.user = auth.user;
const parseQuery = new Parse.Query(className);
parseQuery.withJSON(request.query);
request.query = parseQuery;
await runTrigger(trigger, `beforeSubscribe.${className}`, request, auth);
const query = request.query.toJSON();
if (query.keys) {
query.fields = query.keys.split(',');
}
request.query = query;
}
// Get subscription from subscriptions, create one if necessary
const subscriptionHash = queryHash(request.query);

View File

@@ -173,7 +173,7 @@ export class FunctionsRouter extends PromiseRouter {
);
return Promise.resolve()
.then(() => {
return triggers.maybeRunValidator(request, functionName);
return triggers.maybeRunValidator(request, functionName, req.auth);
})
.then(() => {
return theFunction(request);

View File

@@ -751,6 +751,9 @@ module.exports = ParseCloud;
* @property {Array|function|Any} requireUserKeys.field.options array of options that the field can be, function to validate field, or single value. Throw an error if value is invalid.
* @property {String} requireUserKeys.field.error custom error message if field is invalid.
*
* @property {Array<String>|function}requireAnyUserRoles If set, request.user has to be part of at least one roles name to make the request. If set to a function, function must return role names.
* @property {Array<String>|function}requireAllUserRoles If set, request.user has to be part all roles name to make the request. If set to a function, function must return role names.
*
* @property {Object|Array<String>} fields if an array of strings, validator will look for keys in request.params, and throw if not provided. If Object, fields to validate. If the trigger is a cloud function, `request.params` will be validated, otherwise `request.object`.
* @property {String} fields.field name of field to validate.
* @property {String} fields.field.type expected type of data for field.

View File

@@ -168,6 +168,17 @@ export function getTrigger(className, triggerType, applicationId) {
return get(Category.Triggers, `${triggerType}.${className}`, applicationId);
}
export async function runTrigger(trigger, name, request, auth) {
if (!trigger) {
return;
}
await maybeRunValidator(request, name, auth);
if (request.skipWithMasterKey) {
return;
}
return await trigger(request);
}
export function getFileTrigger(type, applicationId) {
return getTrigger(FileClassName, type, applicationId);
}
@@ -424,7 +435,7 @@ export function maybeRunAfterFindTrigger(
});
return Promise.resolve()
.then(() => {
return maybeRunValidator(request, `${triggerType}.${className}`);
return maybeRunValidator(request, `${triggerType}.${className}`, auth);
})
.then(() => {
if (request.skipWithMasterKey) {
@@ -489,7 +500,7 @@ export function maybeRunQueryTrigger(
);
return Promise.resolve()
.then(() => {
return maybeRunValidator(requestObject, `${triggerType}.${className}`);
return maybeRunValidator(requestObject, `${triggerType}.${className}`, auth);
})
.then(() => {
if (requestObject.skipWithMasterKey) {
@@ -591,7 +602,7 @@ export function resolveError(message, defaultOpts) {
}
return error;
}
export function maybeRunValidator(request, functionName) {
export function maybeRunValidator(request, functionName, auth) {
const theValidator = getValidator(functionName, Parse.applicationId);
if (!theValidator) {
return;
@@ -603,7 +614,7 @@ export function maybeRunValidator(request, functionName) {
return Promise.resolve()
.then(() => {
return typeof theValidator === 'object'
? builtInTriggerValidator(theValidator, request)
? builtInTriggerValidator(theValidator, request, auth)
: theValidator(request);
})
.then(() => {
@@ -618,7 +629,7 @@ export function maybeRunValidator(request, functionName) {
});
});
}
function builtInTriggerValidator(options, request) {
async function builtInTriggerValidator(options, request, auth) {
if (request.master && !options.validateMasterKey) {
return;
}
@@ -631,7 +642,10 @@ function builtInTriggerValidator(options, request) {
) {
reqUser = request.object;
}
if (options.requireUser && !reqUser) {
if (
(options.requireUser || options.requireAnyUserRoles || options.requireAllUserRoles) &&
!reqUser
) {
throw 'Validation failed. Please login to continue.';
}
if (options.requireMaster && !request.master) {
@@ -722,6 +736,38 @@ function builtInTriggerValidator(options, request) {
}
}
}
let userRoles = options.requireAnyUserRoles;
let requireAllRoles = options.requireAllUserRoles;
const promises = [Promise.resolve(), Promise.resolve(), Promise.resolve()];
if (userRoles || requireAllRoles) {
promises[0] = auth.getUserRoles();
}
if (typeof userRoles === 'function') {
promises[1] = userRoles();
}
if (typeof requireAllRoles === 'function') {
promises[2] = requireAllRoles();
}
const [roles, resolvedUserRoles, resolvedRequireAll] = await Promise.all(promises);
if (resolvedUserRoles && Array.isArray(resolvedUserRoles)) {
userRoles = resolvedUserRoles;
}
if (resolvedRequireAll && Array.isArray(resolvedRequireAll)) {
requireAllRoles = resolvedRequireAll;
}
if (userRoles) {
const hasRole = userRoles.some(requiredRole => roles.includes(`role:${requiredRole}`));
if (!hasRole) {
throw `Validation failed. User does not match the required roles.`;
}
}
if (requireAllRoles) {
for (const requiredRole of requireAllRoles) {
if (!roles.includes(`role:${requiredRole}`)) {
throw `Validation failed. User does not match all the required roles.`;
}
}
}
const userKeys = options.requireUserKeys || [];
if (Array.isArray(userKeys)) {
for (const key of userKeys) {
@@ -809,7 +855,7 @@ export function maybeRunTrigger(
// to the RestWrite.execute() call.
return Promise.resolve()
.then(() => {
return maybeRunValidator(request, `${triggerType}.${parseObject.className}`);
return maybeRunValidator(request, `${triggerType}.${parseObject.className}`, auth);
})
.then(() => {
if (request.skipWithMasterKey) {
@@ -890,7 +936,7 @@ export async function maybeRunFileTrigger(triggerType, fileObject, config, auth)
if (typeof fileTrigger === 'function') {
try {
const request = getRequestFileObject(triggerType, auth, fileObject, config);
await maybeRunValidator(request, `${triggerType}.${FileClassName}`);
await maybeRunValidator(request, `${triggerType}.${FileClassName}`, auth);
if (request.skipWithMasterKey) {
return fileObject;
}
@@ -916,70 +962,3 @@ export async function maybeRunFileTrigger(triggerType, fileObject, config, auth)
}
return fileObject;
}
export async function maybeRunConnectTrigger(triggerType, request) {
const trigger = getTrigger(ConnectClassName, triggerType, Parse.applicationId);
if (!trigger) {
return;
}
request.user = await userForSessionToken(request.sessionToken);
await maybeRunValidator(request, `${triggerType}.${ConnectClassName}`);
if (request.skipWithMasterKey) {
return;
}
return trigger(request);
}
export async function maybeRunSubscribeTrigger(triggerType, className, request) {
const trigger = getTrigger(className, triggerType, Parse.applicationId);
if (!trigger) {
return;
}
const parseQuery = new Parse.Query(className);
parseQuery.withJSON(request.query);
request.query = parseQuery;
request.user = await userForSessionToken(request.sessionToken);
await maybeRunValidator(request, `${triggerType}.${className}`);
if (request.skipWithMasterKey) {
return;
}
await trigger(request);
const query = request.query.toJSON();
if (query.keys) {
query.fields = query.keys.split(',');
}
request.query = query;
}
export async function maybeRunAfterEventTrigger(triggerType, className, request) {
const trigger = getTrigger(className, triggerType, Parse.applicationId);
if (!trigger) {
return;
}
if (request.object) {
request.object = Parse.Object.fromJSON(request.object);
}
if (request.original) {
request.original = Parse.Object.fromJSON(request.original);
}
request.user = await userForSessionToken(request.sessionToken);
await maybeRunValidator(request, `${triggerType}.${className}`);
if (request.skipWithMasterKey) {
return;
}
return trigger(request);
}
async function userForSessionToken(sessionToken) {
if (!sessionToken) {
return;
}
const q = new Parse.Query('_Session');
q.equalTo('sessionToken', sessionToken);
q.include('user');
const session = await q.first({ useMasterKey: true });
if (!session) {
return;
}
return session.get('user');
}