jjzjj

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

coder 2024-07-16 原文

我正在尝试向 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)
);

有问题的 API 端点是 GET/users/:user/teams,它返回用户所属的所有团队。该路由的 Controller 如下所示:

(注意:所有这些都是 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
}

这四个异步函数是授权“完成”所需发生的核心逻辑步骤。以下是这些函数中的每一个大致的样子:

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

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')
}

canFindTeams 确保当前用户是发出请求的用户,或者当前用户是相关用户的队友。不应授权其他任何人查找相关用户。在我的实际实现中,它实际上是通过具有关联的操作roles完成的,这样队友可以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
  `)
}

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
}

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

问题

导致我像这样分解它的一个要求是我需要一种方法来抛出那些特定的错误代码,以便返回给 API 客户端的错误是有帮助的。例如,exists 函数在 canFindTeams 函数之前运行,因此并非所有错误都显示 403 Unauthorized

另一个,这里没有用伪代码很好地传达,是 currentUser 实际上可以是一个 app(第三方客户端),一个 team(属于团队本身的访问 token )或 user(常见情况)。此要求使得难以将 canFindTeamsmaskTeams 函数作为单个 SQL 语句来实现,因为逻辑必须以三种方式分支...在我的实现中,这两个函数实际上是围绕处理所有三种情况的逻辑的 switch 语句——请求者是 appteamuser

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

问题

  • 额外的查询是否会对性能产生有意义的影响?
  • 能否将它们轻松组合成更少的查询?
  • 是否有更好的授权设计来简化这一过程?
  • 不使用交易会带来问题吗?
  • 您还有什么要更改的吗?

谢谢!

最佳答案

我把它变成了一个函数并简化了表格,以便于测试。 SQL Fiddle .我正在做假设,因为一些规则嵌入在我不太理解的 javascript 伪代码中。

create or replace function visible_teams (
    _user_id int, _current_user_id int
) returns table (
    current_user_role int,
    team_id int,
    team_email text,
    team_name text,
    team_created_at date
) as $$
    select
        m0.role,
        m0.team,
        t.email,
        t.name,
        t.created_at
    from
        memberships m0
        left join
        memberships m1 on m0.team = m1.team and m1.user = _user_id
        inner join
        teams t on t.id = m0.team
    where m0.user = _current_user_id

    union

    select null, null, null, null, null
    where not exists (select 1 from users where id = _user_id)

    order by role nulls first
    ;
$$ language sql;

返回所有当前用户的团队加上用户共同的团队:

select * from visible_teams(3, 1);
 current_user_role | team_id | team_email | team_name | team_created_at 
-------------------+---------+------------+-----------+-----------------
                 1 |       1 | email_1    | team_1    | 2016-03-13
                 1 |       3 | email_3    | team_3    | 2016-03-13
                 2 |       2 | email_2    | team_2    | 2016-03-13
(3 rows)

当用户不存在时,它返回包含空值的第一行以及所有当前用户的团队:

select * from visible_teams(5, 1);
 current_user_role | team_id | team_email | team_name | team_created_at 
-------------------+---------+------------+-----------+-----------------
                   |         |            |           | 
                 1 |       1 | email_1    | team_1    | 2016-03-13
                 1 |       3 | email_3    | team_3    | 2016-03-13
                 2 |       2 | email_2    | team_2    | 2016-03-13
(4 rows)

当当前用户不存在则空集:

select * from visible_teams(1, 5);
 current_user_role | team_id | team_email | team_name | team_created_at 
-------------------+---------+------------+-----------+-----------------
(0 rows)

关于javascript - 如何在更少的 SQL 查询中执行复杂的 API 授权?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/35910451/

有关javascript - 如何在更少的 SQL 查询中执行复杂的 API 授权?的更多相关文章

  1. ruby - 如何在 Ruby 中顺序创建 PI - 2

    出于纯粹的兴趣,我很好奇如何按顺序创建PI,而不是在过程结果之后生成数字,而是让数字在过程本身生成时显示。如果是这种情况,那么数字可以自行产生,我可以对以前看到的数字实现垃圾收集,从而创建一个无限系列。结果只是在Pi系列之后每秒生成一个数字。这是我通过互联网筛选的结果:这是流行的计算机友好算法,类机器算法:defarccot(x,unity)xpow=unity/xn=1sign=1sum=0loopdoterm=xpow/nbreakifterm==0sum+=sign*(xpow/n)xpow/=x*xn+=2sign=-signendsumenddefcalc_pi(digits

  2. ruby-openid:执行发现时未设置@socket - 2

    我在使用omniauth/openid时遇到了一些麻烦。在尝试进行身份验证时,我在日志中发现了这一点:OpenID::FetchingError:Errorfetchinghttps://www.google.com/accounts/o8/.well-known/host-meta?hd=profiles.google.com%2Fmy_username:undefinedmethod`io'fornil:NilClass重要的是undefinedmethodio'fornil:NilClass来自openid/fetchers.rb,在下面的代码片段中:moduleNetclass

  3. ruby - ECONNRESET (Whois::ConnectionError) - 尝试在 Ruby 中查询 Whois 时出错 - 2

    我正在用Ruby编写一个简单的程序来检查域列表是否被占用。基本上它循环遍历列表,并使用以下函数进行检查。require'rubygems'require'whois'defcheck_domain(domain)c=Whois::Client.newc.query("google.com").available?end程序不断出错(即使我在google.com中进行硬编码),并打印以下消息。鉴于该程序非常简单,我已经没有什么想法了-有什么建议吗?/Library/Ruby/Gems/1.8/gems/whois-2.0.2/lib/whois/server/adapters/base.

  4. ruby - 如何在 buildr 项目中使用 Ruby 代码? - 2

    如何在buildr项目中使用Ruby?我在很多不同的项目中使用过Ruby、JRuby、Java和Clojure。我目前正在使用我的标准Ruby开发一个模拟应用程序,我想尝试使用Clojure后端(我确实喜欢功能代码)以及JRubygui和测试套件。我还可以看到在未来的不同项目中使用Scala作为后端。我想我要为我的项目尝试一下buildr(http://buildr.apache.org/),但我注意到buildr似乎没有设置为在项目中使用JRuby代码本身!这看起来有点傻,因为该工具旨在统一通用的JVM语言并且是在ruby中构建的。除了将输出的jar包含在一个独特的、仅限ruby​​

  5. ruby - 什么是填充的 Base64 编码字符串以及如何在 ruby​​ 中生成它们? - 2

    我正在使用的第三方API的文档状态:"[O]urAPIonlyacceptspaddedBase64encodedstrings."什么是“填充的Base64编码字符串”以及如何在Ruby中生成它们。下面的代码是我第一次尝试创建转换为Base64的JSON格式数据。xa=Base64.encode64(a.to_json) 最佳答案 他们说的padding其实就是Base64本身的一部分。它是末尾的“=”和“==”。Base64将3个字节的数据包编码为4个编码字符。所以如果你的输入数据有长度n和n%3=1=>"=="末尾用于填充n%

  6. ruby-on-rails - 如何在 ruby​​ 中使用两个参数异步运行 exe? - 2

    exe应该在我打开页面时运行。异步进程需要运行。有什么方法可以在ruby​​中使用两个参数异步运行exe吗?我已经尝试过ruby​​命令-system()、exec()但它正在等待过程完成。我需要用参数启动exe,无需等待进程完成是否有任何ruby​​gems会支持我的问题? 最佳答案 您可以使用Process.spawn和Process.wait2:pid=Process.spawn'your.exe','--option'#Later...pid,status=Process.wait2pid您的程序将作为解释器的子进程执行。除

  7. ruby - 如何在续集中重新加载表模式? - 2

    鉴于我有以下迁移:Sequel.migrationdoupdoalter_table:usersdoadd_column:is_admin,:default=>falseend#SequelrunsaDESCRIBEtablestatement,whenthemodelisloaded.#Atthispoint,itdoesnotknowthatusershaveais_adminflag.#Soitfails.@user=User.find(:email=>"admin@fancy-startup.example")@user.is_admin=true@user.save!ende

  8. ruby - 如何在 Ruby 中拆分参数字符串 Bash 样式? - 2

    我正在为一个项目制作一个简单的shell,我希望像在Bash中一样解析参数字符串。foobar"helloworld"fooz应该变成:["foo","bar","helloworld","fooz"]等等。到目前为止,我一直在使用CSV::parse_line,将列分隔符设置为""和.compact输出。问题是我现在必须选择是要支持单引号还是双引号。CSV不支持超过一个分隔符。Python有一个名为shlex的模块:>>>shlex.split("Test'helloworld'foo")['Test','helloworld','foo']>>>shlex.split('Test"

  9. ruby - 如何在 Lion 上安装 Xcode 4.6,需要用 RVM 升级 ruby - 2

    我实际上是在尝试使用RVM在我的OSX10.7.5上更新ruby,并在输入以下命令后:rvminstallruby我得到了以下回复:Searchingforbinaryrubies,thismighttakesometime.Checkingrequirementsforosx.Installingrequirementsforosx.Updatingsystem.......Errorrunning'requirements_osx_brew_update_systemruby-2.0.0-p247',pleaseread/Users/username/.rvm/log/138121

  10. ruby - Chef 执行非顺序配方 - 2

    我遵循了教程http://gettingstartedwithchef.com/,第1章。我的运行list是"run_list":["recipe[apt]","recipe[phpap]"]我的phpapRecipe默认Recipeinclude_recipe"apache2"include_recipe"build-essential"include_recipe"openssl"include_recipe"mysql::client"include_recipe"mysql::server"include_recipe"php"include_recipe"php::modul

随机推荐