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

feat: whoami granular #494

Merged
merged 1 commit into from
Jun 4, 2023
Merged
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
4 changes: 2 additions & 2 deletions app/core/entity/Token.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ interface BaseTokenData extends EntityData {
cidrWhitelist?: string[];
userId: string;
isReadonly?: boolean;
type?: TokenType;
type?: TokenType | string;
lastUsedAt?: Date;
}

Expand Down Expand Up @@ -61,7 +61,7 @@ export class Token extends Entity {
this.tokenKey = data.tokenKey;
this.cidrWhitelist = data.cidrWhitelist || [];
this.isReadonly = data.isReadonly || false;
this.type = data.type || TokenType.classic;
this.type = (data.type as TokenType) || TokenType.classic;
this.lastUsedAt = data.lastUsedAt || null;

if (isGranularToken(data)) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这段代码补丁看起来没有明显的错误风险,但是以下是一些可以改进的建议:

  1. 为了提高代码的可读性和易于维护性,可以向 interface BaseTokenData 中添加 API 文档注释。
  2. data.type 的类型不是 TokenType 时,将其默认值更改为 null 或引发错误可能会更好,并将实例化 Token 对象的逻辑清晰化,在构造函数中分别处理默认值、可选值和必需值将有助于代码可读性上的改进。

Expand Down
10 changes: 0 additions & 10 deletions app/port/controller/TokenController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ import {
import { Static, Type } from '@sinclair/typebox';
import { AbstractController } from './AbstractController';
import { TokenType, isGranularToken } from '../../core/entity/Token';
import { TokenService } from '../../../app/core/service/TokenService';
import { getFullname } from '../../../app/common/PackageUtil';

// Creating and viewing access tokens
// https://docs.npmjs.com/creating-and-viewing-access-tokens#viewing-access-tokens
Expand Down Expand Up @@ -44,8 +42,6 @@ type GranularTokenOptions = Static<typeof GranularTokenOptionsRule>;
export class TokenController extends AbstractController {
@Inject()
private readonly authAdapter: AuthAdapter;
@Inject()
private readonly tokenService: TokenService;
// https://github.com/npm/npm-profile/blob/main/lib/index.js#L233
@HTTPMethod({
path: '/-/npm/v1/tokens',
Expand Down Expand Up @@ -198,12 +194,6 @@ export class TokenController extends AbstractController {
const tokens = await this.userRepository.listTokens(user.userId);
const granularTokens = tokens.filter(token => isGranularToken(token));

for (const token of granularTokens) {
const packages = await this.tokenService.listTokenPackages(token);
if (Array.isArray(packages)) {
token.allowedPackages = packages.map(p => getFullname(p.scope, p.name));
}
}
const objects = granularTokens.map(token => {
const { name, description, expiredAt, allowedPackages, allowedScopes, lastUsedAt, type } = token;
return {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这段代码对TokenController进行了修改,删除了TokenService和getFullname的调用。可能存在一些潜在问题和可优化空间。建议将以下问题考虑在内:

  1. 删除TokenService和getFullname的代码段是否意味着需要在其他地方引入它们的实现?

  2. granularTokens.map会产生一个新数组,如果tokens数组过大,可能会导致内存溢出,建议使用可迭代的解决方案。

  3. 是否应该添加一些错误处理来避免可能的异常情况?

Expand Down
29 changes: 27 additions & 2 deletions app/port/controller/UserController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { Static, Type } from '@sinclair/typebox';
import { AbstractController } from './AbstractController';
import { LoginResultCode } from '../../common/enum/User';
import { sha512 } from '../../common/UserUtil';
import { isGranularToken } from '../../core/entity/Token';

// body: {
// _id: 'org.couchdb.user:dddd',
Expand Down Expand Up @@ -157,10 +158,34 @@ export class UserController extends AbstractController {
method: HTTPMethodEnum.GET,
})
async whoami(@Context() ctx: EggContext) {
const authorizedUser = await this.userRoleManager.requiredAuthorizedUser(ctx, 'read');
await this.userRoleManager.requiredAuthorizedUser(ctx, 'read');
const authorizedRes = await this.userRoleManager.getAuthorizedUserAndToken(ctx);
const { token, user } = authorizedRes!;

if (isGranularToken(token)) {
const { name, description, expiredAt, allowedPackages, allowedScopes, lastUsedAt, type } = token;
return {
username: user.displayName,
name,
description,
allowedPackages,
allowedScopes,
lastUsedAt,
expiredAt,
// do not return token value
// token: token.token,
key: token.tokenKey,
cidr_whitelist: token.cidrWhitelist,
readonly: token.isReadonly,
created: token.createdAt,
updated: token.updatedAt,
type,
};
}
return {
username: authorizedUser.displayName,
username: user.displayName,
};

}

// https://github.com/cnpm/cnpmcore/issues/64
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这段代码主要是一个用户控制器,包含了一些处理用户登陆、权限等的方法。对于提供的代码补丁,以下是我的小型代码审查:

  • 在第 4 行,import 引入了新模块 isGranularToken,需要确定此模块是否存在和正确性。
  • 在第 157 行,在获取 authorizedUser 后在必要时应该对其进行空值检查以避免潜在的 NullReference 错误。
  • 在第 158 至 187 行中新增了 whoami 方法,返回已授权用户的相关信息。其中,使用了类型为 GranularToken 的 token 对象,并只返回部分属性。需要确认 GranularToken 对象有否定义完整并符合预期。
  • 返回数据可以加上描述信息,增强代码阅读的可理解性。

建议:

  • 除非 isGranularToken 已由其他模块导出或定义,否则应考虑将其放在同一文件夹下进行维护和管理。
  • 设置控制器的分层结构。
  • 确保所引用的外部依赖库已正确安装,并针对在执行测例时可能出现的所有异常情况进行单元测试。

总的来说,这段代码看起来良好且易于理解,但需要专注于特殊的对象和方法,并仔细验证所有新添加的内容。

Expand Down
27 changes: 24 additions & 3 deletions app/repository/UserRepository.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { AccessLevel, SingletonProto, Inject } from '@eggjs/tegg';
import { ModelConvertor } from './util/ModelConvertor';
import type { User as UserModel } from './model/User';
import type { Package as PackageModel } from './model/Package';
import type { Token as TokenModel } from './model/Token';
import type { WebauthnCredential as WebauthnCredentialModel } from './model/WebauthnCredential';
import { User as UserEntity } from '../core/entity/User';
import { Token as TokenEntity, isGranularToken } from '../core/entity/Token';
import { WebauthnCredential as WebauthnCredentialEntity } from '../core/entity/WebauthnCredential';
import { AbstractRepository } from './AbstractRepository';
import { TokenPackage as TokenPackageModel } from './model/TokenPackage';
import { getScopeAndName } from '../common/PackageUtil';
import { getFullname, getScopeAndName } from '../common/PackageUtil';
import { PackageRepository } from './PackageRepository';

@SingletonProto({
Expand All @@ -24,6 +25,9 @@ export class UserRepository extends AbstractRepository {
@Inject()
private readonly TokenPackage: typeof TokenPackageModel;

@Inject()
private readonly Package: typeof PackageModel;

@Inject()
private readonly packageRepository: PackageRepository;

Expand Down Expand Up @@ -58,6 +62,7 @@ export class UserRepository extends AbstractRepository {
if (!token) return null;
const userModel = await this.User.findOne({ userId: token.userId });
if (!userModel) return null;

return {
token,
user: ModelConvertor.convertModelToEntity(userModel, UserEntity),
Expand All @@ -67,7 +72,19 @@ export class UserRepository extends AbstractRepository {
async findTokenByTokenKey(tokenKey: string) {
const model = await this.Token.findOne({ tokenKey });
if (!model) return null;
return ModelConvertor.convertModelToEntity(model, TokenEntity);
const token = ModelConvertor.convertModelToEntity(model, TokenEntity);
await this._injectTokenPackages(token);
return token;
}

private async _injectTokenPackages(token: TokenEntity) {
if (isGranularToken(token)) {
const models = await this.TokenPackage.find({ tokenId: token.tokenId });
const packages = await this.Package.find({ packageId: models.map(m => m.packageId) });
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

希望未来别一个 token 有几十万个包

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

https://github.com/cnpm/cnpmcore/blob/master/app/port/controller/TokenController.ts#L36

现在做了最大 50 个的限制,等 npm 后面调整了我们再跟进 😄

if (Array.isArray(packages)) {
token.allowedPackages = packages.map(p => getFullname(p.scope, p.name));
}
}
}

async saveToken(token: TokenEntity): Promise<void> {
Expand Down Expand Up @@ -111,7 +128,11 @@ export class UserRepository extends AbstractRepository {

async listTokens(userId: string): Promise<TokenEntity[]> {
const models = await this.Token.find({ userId });
return models.map(model => ModelConvertor.convertModelToEntity(model, TokenEntity));
const tokens = models.map(model => ModelConvertor.convertModelToEntity(model, TokenEntity));
for (const token of tokens) {
await this._injectTokenPackages(token);
}
return tokens;
}

async saveCredential(credential: WebauthnCredentialEntity): Promise<void> {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

这个代码补丁主要是对一个用户仓库 UserRepository 进行了调整,添加了一个新的模型依赖注入,并在私有方法 _injectTokenPackages() 中使用。这个方法会获取特定 token 的详细信息,包括它所属的 package 信息,并将这些信息添加到返回的 token 实体中。

回顾代码,以下问题可能需要注意:

  • 没有看到对 AbstractRepository 类的定义,因此无法判断属性和方法是否按照预期工作。
  • 对于返回值的类型,可以尝试使用更明确的类型或接口而不是泛型,例如 Promise<TokenEntity | null> 可以改写为 Promise<{ token: TokenEntity; user: UserEntity } | null>
  • findTokenByTokenKey() 方法中,如果找不到相应的 model ,应该返回 Promise.resolve(null) 而不是直接返回 null 。
  • 私有方法 _injectTokenPackages() 只在单个方法中调用。因此,您可以删除私有方法并将其内容放到 findTokenByTokenKey()listTokens() 方法之中,或者将之提取出来作为公共方法,以便其他仓库也可以使用。

Expand Down
39 changes: 37 additions & 2 deletions test/port/controller/UserController/whoami.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { AuthAdapter } from 'app/infra/AuthAdapter';
import assert from 'assert';
import { app } from 'egg-mock/bootstrap';
import dayjs from 'dayjs';
import { app, mock } from 'egg-mock/bootstrap';
import { TestUtil } from 'test/TestUtil';

describe('test/port/controller/UserController/whoami.test.ts', () => {
Expand All @@ -10,7 +12,7 @@ describe('test/port/controller/UserController/whoami.test.ts', () => {
.get('/-/whoami')
.set('authorization', authorization)
.expect(200);
assert.equal(res.body.username, name);
assert.deepStrictEqual(res.body, { username: name });
});

it('should unauthorized', async () => {
Expand All @@ -25,5 +27,38 @@ describe('test/port/controller/UserController/whoami.test.ts', () => {
.expect(401);
assert.equal(res.body.error, '[UNAUTHORIZED] Invalid token');
});

it('should return granular token info', async () => {
const { name, email } = await TestUtil.createUser({ name: 'banana' });
await TestUtil.createPackage({ name: '@cnpm/banana' });
mock(AuthAdapter.prototype, 'ensureCurrentUser', async () => {
return {
name,
email,
};
});
let res = await app.httpRequest()
.post('/-/npm/v1/tokens/gat')
.send({
name: 'apple',
description: 'lets play',
allowedPackages: [ '@cnpm/banana' ],
allowedScopes: [ '@banana' ],
expires: 30,
})
.expect(200);

res = await app.httpRequest()
.get('/-/whoami')
.set('authorization', `Bearer ${res.body.token}`)
.expect(200);

assert.equal(res.body.username, name);
assert.equal(res.body.name, 'apple');
assert.equal(res.body.description, 'lets play');
assert.deepEqual(res.body.allowedPackages, [ '@cnpm/banana' ]);
assert.deepEqual(res.body.allowedScopes, [ '@banana' ]);
assert(dayjs(res.body.expires).isBefore(dayjs().add(30, 'days')));
});
});
});
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

此代码段主要是针对一个Node.js应用中UserController的whoami方法编写的测试代码。这里引入了AuthAdapter库,新加入了dayjs和mock库。

建议对新增内容进行以下改进:

  • 需要在代码开始部分添加必要的import语句。
  • 同时添加用于控制测试范围的边界条件。例如,在测试函数开头添加beforeEach和afterEach标记,以便能够在测试之间设置并清理使用的资源。
  • 在进行模拟调用时,需要注意不要篡改其他方法,可以通过创建新实例或仅返回所需数据进行优化。
  • 建议在测试用例说明中注明测试对象和测试点。