mirror of
https://github.com/gristlabs/grist-core.git
synced 2026-03-02 04:09:24 +00:00
(core) Adding new column in users table "ref" with unique identifier.
Summary: There is a new column in users table called ref (user reference). It holds user's unique reference number that can be used for features that require some kind of ownership logic (like comments). Test Plan: Updated tests Reviewers: georgegevoian, paulfitz Reviewed By: georgegevoian, paulfitz Differential Revision: https://phab.getgrist.com/D3641
This commit is contained in:
@@ -528,33 +528,46 @@ export interface Authorizer {
|
||||
getCachedAuth(): DocAuthResult;
|
||||
}
|
||||
|
||||
export interface DocAuthorizerOptions {
|
||||
dbManager: HomeDBManager;
|
||||
key: DocAuthKey;
|
||||
openMode: OpenDocMode;
|
||||
linkParameters: Record<string, string>;
|
||||
userRef?: string|null;
|
||||
docAuth?: DocAuthResult;
|
||||
profile?: UserProfile;
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* Handle authorization for a single document and user.
|
||||
*
|
||||
*/
|
||||
export class DocAuthorizer implements Authorizer {
|
||||
public readonly openMode: OpenDocMode;
|
||||
public readonly linkParameters: Record<string, string>;
|
||||
constructor(
|
||||
private _dbManager: HomeDBManager,
|
||||
private _key: DocAuthKey,
|
||||
public readonly openMode: OpenDocMode,
|
||||
public readonly linkParameters: Record<string, string>,
|
||||
private _docAuth?: DocAuthResult,
|
||||
private _profile?: UserProfile
|
||||
private _options: DocAuthorizerOptions
|
||||
) {
|
||||
this.openMode = _options.openMode;
|
||||
this.linkParameters = _options.linkParameters;
|
||||
}
|
||||
|
||||
public getUserId(): number {
|
||||
return this._key.userId;
|
||||
return this._options.key.userId;
|
||||
}
|
||||
|
||||
public getUser(): FullUser|null {
|
||||
return this._profile ? {id: this.getUserId(), ...this._profile} : null;
|
||||
return this._options.profile ? {
|
||||
id: this.getUserId(),
|
||||
ref: this._options.userRef,
|
||||
...this._options.profile
|
||||
} : null;
|
||||
}
|
||||
|
||||
public getDocId(): string {
|
||||
// We've been careful to require urlId === docId, see DocManager.
|
||||
return this._key.urlId;
|
||||
return this._options.key.urlId;
|
||||
}
|
||||
|
||||
public getLinkParameters(): Record<string, string> {
|
||||
@@ -562,18 +575,18 @@ export class DocAuthorizer implements Authorizer {
|
||||
}
|
||||
|
||||
public async getDoc(): Promise<Document> {
|
||||
return this._dbManager.getDoc(this._key);
|
||||
return this._options.dbManager.getDoc(this._options.key);
|
||||
}
|
||||
|
||||
public async assertAccess(role: 'viewers'|'editors'|'owners'): Promise<void> {
|
||||
const docAuth = await this._dbManager.getDocAuthCached(this._key);
|
||||
this._docAuth = docAuth;
|
||||
const docAuth = await this._options.dbManager.getDocAuthCached(this._options.key);
|
||||
this._options.docAuth = docAuth;
|
||||
assertAccess(role, docAuth, {openMode: this.openMode});
|
||||
}
|
||||
|
||||
public getCachedAuth(): DocAuthResult {
|
||||
if (!this._docAuth) { throw Error('no cached authentication'); }
|
||||
return this._docAuth;
|
||||
if (!this._options.docAuth) { throw Error('no cached authentication'); }
|
||||
return this._options.docAuth;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -87,6 +87,7 @@ export class Client {
|
||||
private _profile: UserProfile|null = null;
|
||||
private _userId: number|null = null;
|
||||
private _userName: string|null = null;
|
||||
private _userRef: string|null = null;
|
||||
private _firstLoginAt: Date|null = null;
|
||||
private _isAnonymous: boolean = false;
|
||||
private _nextSeqId: number = 0; // Next sequence-ID for messages sent to the client
|
||||
@@ -388,25 +389,26 @@ export class Client {
|
||||
return this._userId;
|
||||
}
|
||||
|
||||
public getCachedUserRef(): string|null {
|
||||
return this._userRef;
|
||||
}
|
||||
|
||||
// Returns the userId for profile.email, or null when profile is not set; with caching.
|
||||
public async getUserId(dbManager: HomeDBManager): Promise<number|null> {
|
||||
if (!this._userId) {
|
||||
if (this._profile) {
|
||||
const user = await this._fetchUser(dbManager);
|
||||
this._userId = (user && user.id) || null;
|
||||
this._userName = (user && user.name) || null;
|
||||
this._isAnonymous = this._userId && dbManager.getAnonymousUserId() === this._userId || false;
|
||||
this._firstLoginAt = (user && user.firstLoginAt) || null;
|
||||
} else {
|
||||
this._userId = dbManager.getAnonymousUserId();
|
||||
this._userName = 'Anonymous';
|
||||
this._isAnonymous = true;
|
||||
this._firstLoginAt = null;
|
||||
}
|
||||
await this._refreshUser(dbManager);
|
||||
}
|
||||
return this._userId;
|
||||
}
|
||||
|
||||
// Returns the userRef for profile.email, or null when profile is not set; with caching.
|
||||
public async getUserRef(dbManager: HomeDBManager): Promise<string|null> {
|
||||
if (!this._userRef) {
|
||||
await this._refreshUser(dbManager);
|
||||
}
|
||||
return this._userRef;
|
||||
}
|
||||
|
||||
// Returns the userId for profile.email, or throws 403 error when profile is not set.
|
||||
public async requireUserId(dbManager: HomeDBManager): Promise<number> {
|
||||
const userId = await this.getUserId(dbManager);
|
||||
@@ -431,6 +433,22 @@ export class Client {
|
||||
return meta;
|
||||
}
|
||||
|
||||
private async _refreshUser(dbManager: HomeDBManager) {
|
||||
if (this._profile) {
|
||||
const user = await this._fetchUser(dbManager);
|
||||
this._userId = (user && user.id) || null;
|
||||
this._userName = (user && user.name) || null;
|
||||
this._isAnonymous = this._userId && dbManager.getAnonymousUserId() === this._userId || false;
|
||||
this._firstLoginAt = (user && user.firstLoginAt) || null;
|
||||
this._userRef = user?.ref ?? null;
|
||||
} else {
|
||||
this._userId = dbManager.getAnonymousUserId();
|
||||
this._userName = 'Anonymous';
|
||||
this._isAnonymous = true;
|
||||
this._firstLoginAt = null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes a request from a client. All requests from a client get a response, at least to
|
||||
* indicate success or failure.
|
||||
|
||||
@@ -261,7 +261,7 @@ export class DocManager extends EventEmitter {
|
||||
* `doc` - the object with metadata tables.
|
||||
*/
|
||||
public async openDoc(client: Client, docId: string,
|
||||
mode: OpenDocMode = 'default',
|
||||
openMode: OpenDocMode = 'default',
|
||||
linkParameters: Record<string, string> = {}): Promise<OpenLocalDocResult> {
|
||||
let auth: Authorizer;
|
||||
const dbManager = this._homeDbManager;
|
||||
@@ -271,6 +271,7 @@ export class DocManager extends EventEmitter {
|
||||
const org = client.getOrg();
|
||||
if (!org) { throw new Error('Documents can only be opened in the context of a specific organization'); }
|
||||
const userId = await client.getUserId(dbManager) || dbManager.getAnonymousUserId();
|
||||
const userRef = await client.getUserRef(dbManager);
|
||||
|
||||
// We use docId in the key, and disallow urlId, so we can be sure that we are looking at the
|
||||
// right doc when we re-query the DB over the life of the websocket.
|
||||
@@ -284,7 +285,15 @@ export class DocManager extends EventEmitter {
|
||||
// than a docId.
|
||||
throw new Error(`openDoc expected docId ${docAuth.docId} not urlId ${docId}`);
|
||||
}
|
||||
auth = new DocAuthorizer(dbManager, key, mode, linkParameters, docAuth, client.getProfile() || undefined);
|
||||
auth = new DocAuthorizer({
|
||||
dbManager,
|
||||
key,
|
||||
openMode,
|
||||
linkParameters,
|
||||
userRef,
|
||||
docAuth,
|
||||
profile: client.getProfile() || undefined
|
||||
});
|
||||
} else {
|
||||
log.debug(`DocManager.openDoc not using authorization for ${docId} because GRIST_SINGLE_USER`);
|
||||
auth = new DummyAuthorizer('owners', docId);
|
||||
@@ -302,7 +311,7 @@ export class DocManager extends EventEmitter {
|
||||
|
||||
// If opening in (pre-)fork mode, check if it is appropriate to treat the user as
|
||||
// an owner for granular access purposes.
|
||||
if (mode === 'fork') {
|
||||
if (openMode === 'fork') {
|
||||
if (await activeDoc.canForkAsOwner(docSession)) {
|
||||
// Mark the session specially and flush any cached access
|
||||
// information. It is easier to make this a property of the
|
||||
|
||||
@@ -122,15 +122,17 @@ export function getDocSessionUser(docSession: OptDocSession): FullUser|null {
|
||||
const user = getUser(docSession.req);
|
||||
const email = user.loginEmail;
|
||||
if (email) {
|
||||
return {id: user.id, name: user.name, email};
|
||||
return {id: user.id, name: user.name, email, ref: user.ref};
|
||||
}
|
||||
}
|
||||
if (docSession.client) {
|
||||
const id = docSession.client.getCachedUserId();
|
||||
const ref = docSession.client.getCachedUserRef();
|
||||
const profile = docSession.client.getProfile();
|
||||
if (id && profile) {
|
||||
return {
|
||||
id,
|
||||
ref,
|
||||
...profile
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1608,6 +1608,7 @@ export class GranularAccess implements GranularAccessForBundle {
|
||||
user.Origin = docSession.req?.get('origin') || null;
|
||||
user.SessionID = isAnonymous ? `a${getDocSessionAltSessionId(docSession)}` : `u${user.UserID}`;
|
||||
user.IsLoggedIn = !isAnonymous;
|
||||
user.UserRef = fullUser?.ref || null; // Empty string should be treated as null.
|
||||
|
||||
if (this._ruler.ruleCollection.ruleError && !this._recoveryMode) {
|
||||
// It is important to signal that the doc is in an unexpected state,
|
||||
@@ -2600,6 +2601,7 @@ export class User implements UserInfo {
|
||||
public Origin: string | null = null;
|
||||
public LinkKey: Record<string, string | undefined> = {};
|
||||
public Email: string | null = null;
|
||||
public UserRef: string | null = null;
|
||||
[attribute: string]: any;
|
||||
|
||||
constructor(_info: Record<string, unknown> = {}) {
|
||||
|
||||
Reference in New Issue
Block a user