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

Issue 1219 poc service accounts #1284

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
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
12 changes: 12 additions & 0 deletions app/common/User.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,16 @@ import {getTableId} from 'app/common/DocActions';
import {EmptyRecordView, RecordView} from 'app/common/RecordView';
import {Role} from 'app/common/roles';

/**
* User type to distinguish beetween Users and service accounts
*/
export enum UserTypes {
'login',
'service'
}

export type UserTypesStrings = keyof typeof UserTypes;

/**
* Information about a user, including any user attributes.
*/
Expand All @@ -19,6 +29,7 @@ export interface UserInfo {
* via a share. Otherwise null.
*/
ShareRef: number | null;
Type: UserTypes | null;
[attributes: string]: unknown;
}

Expand All @@ -37,6 +48,7 @@ export class User implements UserInfo {
public SessionID: string | null = null;
public UserRef: string | null = null;
public ShareRef: number | null = null;
public Type: UserTypes | null = null;
[attribute: string]: any;

constructor(info: Record<string, unknown> = {}) {
Expand Down
29 changes: 29 additions & 0 deletions app/gen-server/ApiServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,35 @@ export class ApiServer {
if (data) { this._logDeleteUserEvents(req, data); }
return sendReply(req, res, result);
}));

// POST /service-accounts/
// Creates a new service account attached to the user making the api call.
this._app.post('/api/service-accounts', expressWrap(async (req, res) => {
const userId = getAuthorizedUserId(req);
const serviceAccount: any = await this._dbManager.createServiceAccount(req.body);
return sendOkReply(req, res, {
key:serviceAccount.key,
});
throw new ApiError(`${userId} post Not implemented yet ;)`, 501);
}));

// GET /service-accounts/
// Reads all service accounts attached to the user making the api call.
this._app.get('/api/service-accounts', expressWrap(async (req, res) => {
throw new ApiError('get Not implemented yet ;)', 501);
}));

// GET /service-accounts/:said
// Reads one particular service account of the user making the api call.
this._app.get('/api/service-accounts/:said', expressWrap(async (req, res) => {
throw new ApiError('get by id Not implemented yet ;)', 501);
}));

// DELETE /service-accounts/:said
// Deletes one particular service account of the user making the api call.
this._app.delete('/api/service-accounts/:said', expressWrap(async (req, res) => {
throw new ApiError('delete by id Not implemented yet ;)', 501);
}));
}

private async _getFullUser(req: Request, options: {includePrefs?: boolean} = {}): Promise<FullUser> {
Expand Down
20 changes: 20 additions & 0 deletions app/gen-server/entity/ServiceAccount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {BaseEntity, Column, Entity, PrimaryGeneratedColumn} from "typeorm";

@Entity({name: 'service_accounts'})
export class ServiceAccount extends BaseEntity {

@PrimaryGeneratedColumn()
public id: number;

@Column({type: Number})
public owner_id: number;

@Column({type: Number})
public service_user_id: number;

@Column({type: String})
public description: string;

@Column({type: Date, nullable: false})
public endOfLife: string;
}
12 changes: 12 additions & 0 deletions app/gen-server/entity/User.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import {UserOptions} from 'app/common/UserAPI';
import {UserTypesStrings} from 'app/common/User';
import {nativeValues} from 'app/gen-server/lib/values';
import {makeId} from 'app/server/lib/idUtils';
import {BaseEntity, BeforeInsert, Column, Entity, JoinTable, ManyToMany, OneToMany, OneToOne,
Expand All @@ -8,6 +9,7 @@ import {Group} from "./Group";
import {Login} from "./Login";
import {Organization} from "./Organization";
import {Pref} from './Pref';
import {ServiceAccount} from './ServiceAccount';

@Entity({name: 'users'})
export class User extends BaseEntity {
Expand Down Expand Up @@ -58,12 +60,22 @@ export class User extends BaseEntity {
@Column({name: 'connect_id', type: String, nullable: true})
public connectId: string | null;

@OneToMany(type => User, user => user.serviceAccounts)
@JoinTable({
name: 'service_account_user',
joinColumn: {name: 'user_id'},
inverseJoinColumn: {name: 'service_account_id'}
})
public serviceAccounts: ServiceAccount[];
/**
* Unique reference for this user. Primarily used as an ownership key in a cell metadata (comments).
*/
@Column({name: 'ref', type: String, nullable: false})
public ref: string;

@Column({name: 'type', type: String, default: 'login'})
public type: UserTypesStrings | null;

@BeforeInsert()
public async beforeInsert() {
if (!this.ref) {
Expand Down
14 changes: 14 additions & 0 deletions app/gen-server/lib/homedb/HomeDBManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ import {
} from "typeorm";
import {v4 as uuidv4} from "uuid";
import { GroupsManager } from './GroupsManager';
import { ServiceAccountsManager } from './ServiceAccountsManager';

// Support transactions in Sqlite in async code. This is a monkey patch, affecting
// the prototypes of various TypeORM classes.
Expand Down Expand Up @@ -254,6 +255,7 @@ export type BillingOptions = Partial<Pick<BillingAccount,
export class HomeDBManager extends EventEmitter {
private _usersManager = new UsersManager(this, this._runInTransaction.bind(this));
private _groupsManager = new GroupsManager();
private _serviceAccountsManager = new ServiceAccountsManager(this);
private _connection: DataSource;
private _exampleWorkspaceId: number;
private _exampleOrgId: number;
Expand Down Expand Up @@ -3067,6 +3069,18 @@ export class HomeDBManager extends EventEmitter {
return query.getOne();
}

public async deleteAllServiceAccounts(){
return this._serviceAccountsManager.deleteAllServiceAccounts();
}

public async createServiceAccount(
ownerId: number,
description?: string,
endOfLife?: Date
){
return this._serviceAccountsManager.createServiceAccount(ownerId, description, endOfLife);
}

private _installConfig(
key: ConfigKey,
{ manager }: { manager?: EntityManager }
Expand Down
55 changes: 55 additions & 0 deletions app/gen-server/lib/homedb/ServiceAccountsManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { HomeDBManager } from 'app/gen-server/lib/homedb/HomeDBManager';
//import { UsersManager } from 'app/gen-server/lib/homedb/UsersManager';
import { EntityManager } from 'typeorm';
import { User } from 'app/gen-server/entity/User';
import {v4 as uuidv4} from 'uuid';
//import { ServiceAccount } from 'app/gen-server/entity/ServiceAccount';

export class ServiceAccountsManager {

private get _connection () {
return this._homeDb.connection;
}

public constructor(
private readonly _homeDb: HomeDBManager,
//private _runInTransaction: RunInTransaction
) {}

// This method is implemented for test purpose only
// Using it outside of tests context will lead to partial db
// destruction
public async deleteAllServiceAccounts(optManager?: EntityManager){
const manager = optManager || new EntityManager(this._connection);
const queryBuilder = manager.createQueryBuilder()
.delete()
.from(User, 'users')
.where('users.type ="service"');
return await queryBuilder.execute();
}

public async createServiceAccount(
ownerId: number,
description?: string,
endOfLife?: Date,
){
await this._connection.transaction(async manager => {
//TODO create new service user in order to have its
//id to insert
const uuid = uuidv4();
const email = `${uuid}@serviceaccounts.local`;
const serviceUser = await this._homeDb.getUserByLogin(email);
// FIXME use manager.save(entité);
return await manager.createQueryBuilder()
.insert()
.into('service_accounts')
.values({
ownerId,
serviceUserId: serviceUser.id,
description,
endOfLife,
})
.execute();
});
}
}
6 changes: 5 additions & 1 deletion app/gen-server/lib/homedb/UsersManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import { Pref } from 'app/gen-server/entity/Pref';
import flatten from 'lodash/flatten';
import { EntityManager } from 'typeorm';

import { UserTypesStrings } from 'app/common/User';

// A special user allowed to add/remove the EVERYONE_EMAIL to/from a resource.
export const SUPPORT_EMAIL = appSettings.section('access').flag('supportEmail').requireString({
envVar: 'GRIST_SUPPORT_EMAIL',
Expand Down Expand Up @@ -371,7 +373,7 @@ export class UsersManager {
* unset/outdated fields of an existing record.
*
*/
public async getUserByLogin(email: string, options: GetUserOptions = {}) {
public async getUserByLogin(email: string, options: GetUserOptions = {}, type?: UserTypesStrings) {
const {manager: transaction, profile, userOptions} = options;
const normalizedEmail = normalizeEmail(email);
return await this._runInTransaction(transaction, async manager => {
Expand All @@ -389,6 +391,8 @@ export class UsersManager {
// Special users do not have first time user set so that they don't get redirected to the
// welcome page.
user.isFirstTimeUser = !NON_LOGIN_EMAILS.includes(normalizedEmail);
// redémarrer lundi ici TODO
user.type = typeof type === 'undefined' ? 'login' : type;
login = new Login();
login.email = normalizedEmail;
login.user = user;
Expand Down
76 changes: 76 additions & 0 deletions app/gen-server/migration/1730215435023-ServiceAccounts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import { MigrationInterface, QueryRunner, Table, TableColumn, TableForeignKey, TableIndex } from "typeorm";
import * as sqlUtils from "app/gen-server/sqlUtils";

export class ServiceAccounts1730215435023 implements MigrationInterface {

public async up(queryRunner: QueryRunner): Promise<any> {
const dbType = queryRunner.connection.driver.options.type;
const datetime = sqlUtils.datetime(dbType);

await queryRunner.addColumn('users', new TableColumn({
name: 'type',
type: 'varchar',
isNullable: false,
default: "'login'",
}));
await queryRunner.createTable(
new Table({
name: 'service_accounts',
columns: [
{
name: 'id',
type: 'int',
isPrimary: true,
},
{
name: 'owner_id',
type: 'int',
},
{
name: 'service_user_id',
type: 'int',
},
{
name: 'description',
type: 'varchar',
},
{
name: 'endOfLife',
type: datetime,
isNullable: false,
},
],
})
);
await queryRunner.createForeignKey(
'service_accounts',
new TableForeignKey({
columnNames: ['service_user_id'],
referencedColumnNames: ['id'],
referencedTableName: 'users',
onDelete: 'CASCADE',
})
);
await queryRunner.createForeignKey(
'service_accounts',
new TableForeignKey({
columnNames: ['owner_id'],
referencedColumnNames: ['id'],
referencedTableName: 'users',
onDelete: 'CASCADE',
})
);
await queryRunner.createIndex(
'service_accounts',
new TableIndex({
name: 'service_account__owner',
columnNames: ['service_accounts_owner', 'user_id'],
})
);
}

public async down(queryRunner: QueryRunner): Promise<any> {
await queryRunner.dropColumn('users', 'type');
await queryRunner.dropTable('service_accounts');
}
}
Loading
Loading