如何在更少的SQL查询中执行复杂的API授权?

Ian*_*lor 9 javascript sql database postgresql authorization

我正在尝试向API添加一个授权层,而我当前的设计会产生比我们想要的更多的SQL查询,所以我想知道如何简化这一点.

上下文

以下是此问题的数据库架构:

CREATE TABLE IF NOT EXISTS users (
  id          TEXT PRIMARY KEY,
  email       CITEXT NOT NULL UNIQUE,
  password    TEXT NOT NULL,
  name        TEXT NOT NULL,
  created_at  DATE NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS teams (
  id          TEXT PRIMARY KEY,
  email       CITEXT NOT NULL,
  name        TEXT NOT NULL,
  created_at  DATE NOT NULL DEFAULT CURRENT_TIMESTAMP
);

CREATE TABLE IF NOT EXISTS memberships (
  id          TEXT PRIMARY KEY,
  "user"      TEXT NOT NULL REFERENCES users(id) ON UPDATE CASCADE ON DELETE CASCADE,
  team        TEXT NOT NULL REFERENCES teams(id) ON UPDATE CASCADE ON DELETE CASCADE,
  role        TEXT NOT NULL,
  created_at  DATE NOT NULL DEFAULT CURRENT_TIMESTAMP,
  UNIQUE("user", team)
);
Run Code Online (Sandbox Code Playgroud)

并且有问题的API端点是GET /users/:user/teams,它返回用户所属的所有团队.以下是该路线的控制器:

(注意:所有这些都是Javascript,但为了清晰起见,它有点伪代码.)

async getTeams(currentId, userId) {
  await exists(userId)
  await canFindTeams(currentUser, userId)
  let teams = await findTeams(userId)
  let maskedTeams = await maskTeams(currentUser, teams)
  return maskedTeams
}
Run Code Online (Sandbox Code Playgroud)

这四个异步函数是授权"完成"所需的核心逻辑步骤.以下是每个函数的大致情况:

async exists(userId) {
  let user = await query(`
    SELECT id
    FROM users
    WHERE id = $[userId]
  `)
  if (!user) throw new Error('user_not_found')
  return user
}
Run Code Online (Sandbox Code Playgroud)

exists简单地检查数据库中是否存在用户,userId如果没有,则抛出正确的错误代码.

query 只是用于运行带有转义变量的SQL查询的伪代码.

async canFindTeams(currentUser, userId) {
  if (currentUser.id == userId) return
  let isTeammate = await query(`
    SELECT role
    FROM memberships
    WHERE "user" = $[currentUser.id]
    AND team IN (
      SELECT team
      FROM memberships
      WHERE "user" = $[userId]
    )
  `)
  if (!isTeammate) throw new Error('team_find_unauthorized')
}
Run Code Online (Sandbox Code Playgroud)

canFindTeams确保当前用户是发出请求的用户,或者当前用户是相关用户的队友.不应授权任何其他人找到有问题的用户.在我的实际实现中,它实际上已经完成了roles相关联actions,所以除非他们是自己的,否则一个队友可以teams.read但不能teams.admin.但我简化了这个例子.

async findTeams(userId) {
  return await query(`
    SELECT
      teams.id,
      teams.email,
      teams.name,
      teams.created_at
    FROM teams
    LEFT JOIN memberships ON teams.id = memberships.team
    LEFT JOIN users ON users.id = memberships.user
    WHERE users.id = $[userId]
    ORDER BY
      memberships.created_at DESC,
      teams.id
  `)
}
Run Code Online (Sandbox Code Playgroud)

findTeams 实际上将查询数据库中的团队对象.

async maskTeams(currentUser, teams) {
  let memberships = await query(`
    SELECT team
    FROM memberships
    WHERE "user" = $[currentUser.id]
  `)
  let teamIds = memberships.map(membership => membership.team)
  let maskedTeams = teams.filter(team => teamIds.includes(team.id))
  return maskedTeams
}
Run Code Online (Sandbox Code Playgroud)

maskTeams将仅返回给定用户应该看到的团队.这是必要的,因为用户应该能够看到他们所有的团队,但是队友应该只能看到他们的团队的共同点,以免泄露信息.

问题

导致我这样分解的一个要求是我需要一种方法来抛出那些特定的错误代码,以便返回给API客户端的错误是有帮助的.例如,exists函数在函数之前运行,canFindTeams这样就不会出现任何错误403 Unauthorized.

另一个在伪代码中没有很好地传达的是,currentUser它实际上可以是app(第三方客户端),team(与团队本身相关的访问令牌)或者user(通用情况).这个要求使得很难将单个SQL语句canFindTeamsmaskTeams函数实现为单个SQL语句,因为逻辑必须以三种方式分叉...在我的实现中,这两个函数实际上是围绕逻辑的switch语句,用于处理所有三种情况 - 请求者是一个app,一个team和一个user.

但即使考虑到这些限制,这也需要编写大量额外代码来确保所有这些身份验证要求.我担心性能,代码可维护性,以及这些查询不是全部在单个事务中的事实.

问题

  • 额外的查询是否会对性能产生重大影响?
  • 它们可以轻松组合成更少的查询吗?
  • 是否有更好的授权设计可以简化此操作?
  • 不使用交易会造成问题吗?
  • 还有什么你要改变的吗?

谢谢!

Ian*_*lor 0

I wanted to summarize a few things after having thought about the problem some more and implemented a solution... @rpy\'s answer helped a lot, read that first!

\n\n

There are a few things that are inherent to the authorization code and the database querying code that allow for a better, more future-proof design that lets you get rid of two of those queries.

\n\n

404\'s not 403\'s

\n\n

The first problem, which @rpy alluded to, is that for security purposes, you don\'t want to show users who aren\'t authorized to find an object a 403 response, since it leaks information. Instead, all errors like 403: user_find_unauthorized that are thrown from the code should be remapped (however you want to make that happen) to 404: user_not_found.

\n\n

With that in place, it\'s also pretty easy to change the authorization code to not fail when a user object doesn\'t exist in the first place. (Actually, in my case my authorization code was already structured this way).

\n\n

That lets you get rid of the exists check\xe2\x80\x94one query down.

\n\n

Think About Pagination

\n\n

The second problem is a future problem: what will happen when you decide to add pagination to your API later? With my example code, pagination would be very hard to implement since "querying" and "masking" were separate, such that doing things like LIMIT 10 becomes near impossible to do correctly.

\n\n

For this reason, although the masking code might get complex, you have to include it in your original find query, to allow for pagination LIMIT and ORDER BY clauses.

\n\n

One more query down.

\n\n

2 is Better than 1

\n\n

After all of that, I don\'t think I\'d want to combine the last two queries into a single query, because the separation of concerns between them is very useful. Not only that, but if someone isn\'t authorized to access an object, the current setup will fail fast without the chance that it negatively impacts database load by having to do unnecessary work.

\n\n

With all of that you\'d end up with something along the lines of:

\n\n
async getTeams(currentId, userId) {\n  await can([\'users.find\', \'teams.find\'], currentUser, userId)\n  let teams = await findTeams(currentUser, userId)\n  return teams\n}\n
Run Code Online (Sandbox Code Playgroud)\n\n

can will perform the authorization, and by providing users.find in addition to teams.find it will ensure that unauthorized looks return 404s.

\n\n

findTeams will perform the lookups, and by passing it currentUser it can also incorporate the necessary masking logic.

\n\n

希望对其他对此有疑问的人有所帮助!

\n