YANG QIA 84cfa82304
feat: security (#5923)
* feat: password policy

* feat: password validator

* fix(inbox): i18n of channel title

* feat: atomic counter

* fix: bug

* fix: bloom

* chore: i18n

* fix: counter

* fix: build

* fix: bug

* fix: z-index

* fix: export

* test: add redis cache test

* fix: test

* fix: test

* fix: test

* fix: bug

* fix: form reset

* fix: locale

* fix: version

* fix: separate cache for sub apps

* chore: update
2024-12-29 08:33:27 +08:00

230 lines
6.8 KiB
TypeScript

/**
* This file is part of the NocoBase (R) project.
* Copyright (c) 2020-2024 NocoBase Co., Ltd.
* Authors: NocoBase Team.
*
* This project is dual-licensed under AGPL-3.0 and NocoBase Commercial License.
* For more information, please refer to: https://www.nocobase.com/agreement.
*/
import { Cache } from '@nocobase/cache';
import { Model } from '@nocobase/database';
import { InstallOptions, Plugin } from '@nocobase/server';
import { namespace, presetAuthType, presetAuthenticator } from '../preset';
import authActions from './actions/auth';
import authenticatorsActions from './actions/authenticators';
import { BasicAuth } from './basic-auth';
import { AuthModel } from './model/authenticator';
import { Storer } from './storer';
import { TokenBlacklistService } from './token-blacklist';
import { tval } from '@nocobase/utils';
export class PluginAuthServer extends Plugin {
cache: Cache;
afterAdd() {}
async beforeLoad() {
this.app.db.registerModels({ AuthModel });
}
async load() {
this.cache = await this.app.cacheManager.createCache({
name: 'auth',
prefix: 'auth',
store: 'memory',
});
// Set up auth manager
const storer = new Storer({
db: this.db,
cache: this.cache,
});
this.app.authManager.setStorer(storer);
if (!this.app.authManager.jwt.blacklist) {
// If blacklist service is not set, should configure default blacklist service
this.app.authManager.setTokenBlacklistService(new TokenBlacklistService(this));
}
// register preset auth type
this.app.authManager.registerTypes(presetAuthType, {
auth: BasicAuth,
title: tval('Password', { ns: namespace }),
getPublicOptions: (options) => {
const usersCollection = this.db.getCollection('users');
let signupForm = options?.public?.signupForm || [];
signupForm = signupForm.filter((item: { show: boolean }) => item.show);
if (
!(
signupForm.length &&
signupForm.some(
(item: { field: string; show: boolean; required: boolean }) =>
['username', 'email'].includes(item.field) && item.show && item.required,
)
)
) {
// At least one of the username or email fields is required
signupForm.unshift({ field: 'username', show: true, required: true });
}
signupForm = signupForm
.filter((field: { show: boolean }) => field.show)
.map((item: { field: string; required: boolean }) => {
const field = usersCollection.getField(item.field);
return {
...item,
uiSchema: {
...field.options?.uiSchema,
required: item.required,
},
};
});
return {
...options?.public,
signupForm,
};
},
});
// Register actions
Object.entries(authActions).forEach(
([action, handler]) => this.app.resourceManager.getResource('auth')?.addAction(action, handler),
);
Object.entries(authenticatorsActions).forEach(([action, handler]) =>
this.app.resourceManager.registerActionHandler(`authenticators:${action}`, handler),
);
// Set up ACL
['check', 'signIn', 'signUp'].forEach((action) => this.app.acl.allow('auth', action));
['signOut', 'changePassword'].forEach((action) => this.app.acl.allow('auth', action, 'loggedIn'));
this.app.acl.allow('authenticators', 'publicList');
this.app.acl.registerSnippet({
name: `pm.${this.name}.authenticators`,
actions: ['authenticators:*'],
});
// Change cache when user changed
this.app.db.on('users.afterSave', async (user: Model) => {
const cache = this.app.cache as Cache;
await cache.set(`auth:${user.id}`, user.toJSON());
});
this.app.db.on('users.afterDestroy', async (user: Model) => {
const cache = this.app.cache as Cache;
await cache.del(`auth:${user.id}`);
});
this.app.on('cache:del:auth', async ({ userId }) => {
await this.cache.del(`auth:${userId}`);
});
this.app.auditManager.registerActions([
{
name: 'auth:signIn',
getMetaData: async (ctx: any) => {
let body = {};
if (ctx.status === 200) {
body = {
data: {
...ctx.body.data,
token: undefined,
},
};
} else {
body = ctx.body;
}
return {
request: {
body: {
...ctx.request?.body,
password: undefined,
},
},
};
},
getUserInfo: async (ctx: any) => {
if (!ctx.body?.data?.user) {
return null;
}
// 查询用户角色
const userId = ctx.body.data.user.id;
const user = await ctx.db.getRepository('users').findOne({
filterByTk: userId,
});
const roles = await user?.getRoles();
if (!roles) {
return {
userId,
};
} else {
if (roles.length === 1) {
return {
userId,
roleName: roles[0].name,
};
} else {
// 多角色的情况下暂时不返回角色名
return {
userId,
};
}
}
},
},
{
name: 'auth:signUp',
getMetaData: async (ctx: any) => {
return {
request: {
body: {
...ctx.request?.body,
password: undefined,
confirm_password: undefined,
},
},
};
},
},
{
name: 'auth:changePassword',
getMetaData: async (ctx: any) => {
return {
request: {
body: {},
},
response: {
body: {},
},
};
},
getSourceAndTarget: async (ctx: any) => {
return {
targetCollection: 'users',
targetRecordUK: ctx.auth.user.id,
};
},
},
'auth:signOut',
]);
}
async install(options?: InstallOptions) {
const repository = this.db.getRepository('authenticators');
const exist = await repository.findOne({ filter: { name: presetAuthenticator } });
if (exist) {
return;
}
await repository.create({
values: {
name: presetAuthenticator,
authType: presetAuthType,
description: 'Sign in with username/email.',
enabled: true,
options: {
public: {
allowSignUp: true,
},
},
},
});
}
async remove() {}
}
export default PluginAuthServer;