ceph rgw:bucket policy实现

相比于aws,rgw的bucket policy实现的还不是很完善,有很多细节都不支持,并且已支持的特性也在很多细节方面与s3不同,尤其是因为rgw不支持类似s3的account user结构,而使用tenant作为替代而导致的一些不同。

并且在文档中还提及,为了修正这种不同,以及支持更多特性,在不久后会重写rgw的 Authentication/Authorization subsystem。到时候可能导致一些兼容问题?

差异性,主要有以下几点:

  • 顾名思义,只支持为bucket设置policy,不能将policy设置到user等其他资源上。

  • 指定Principal使用如下格式: "Principal":{"AWS":"arn:aws:iam::<tenant>:user/<username>"};因为目前RGW use ‘tenant’ identifier in place of the Amazon twelve-digit account ID。

  • 在policy json文件中不支持变量的使用,比如${aws:username}

  • rgw支持的Action、Condition是aws的子集,文档中有列出。附录中的RGWListBucket::verify_permission()rgw_build_iam_environment(...)也能看出被支持的Condition有哪些。

  • Under AWS, all tenants share a single namespace. RGW gives every tenant its own namespace of buckets. There may be an option to enable an AWS-like ‘flat’ bucket namespace in future versions. At present, to access a bucket belonging to another tenant, address it as “tenant:bucket” in the S3 request.

  • In AWS, a bucket policy can grant access to another account, and that account owner can then grant access to individual users with user permissions. Since we do not yet support user, role, and group permissions, account owners will currently need to grant access directly to individual users, and granting an entire account access to a bucket grants access to all users in that account.

实现

为一个bucket设置bucket policy,就是向该bucket对应的bucket.instance对象的xattr中以user.rgw.iam-policy为key将上传的policy json文本存入。之后使用时从xattr中查询并解析。

而对于policy的使用,则是在rgw_process.cc/process_request(...)函数中开始的。这个函数就是rgw frontend回调函数最终验证并执行请求的地方,它属于REST API通用处理层,这一层以process_request函数作为入口,其主要步骤大概分为 用户认证、桶/对象acl/policy检查、用户/桶配额检查、执行操作 等。

bucket policy的验证,具体是在process_request调用的rgw_process_authenticated函数中,该函数先后调用了init_permissionsread_permissions,这两个函数都包含读取bucket policy到req_state.iam_policy的语句。

最后在op->verify_permission函数中,根据不同操作进行权限验证,也包括了policy的验证。验证过程大体如下:

  1. 将被验证请求的主体和操作,转换成policy的Principle和Action格式,存入对应的对象,对象变量名分别为ida和res。
  2. 判断ida与bucket policy中的Principle是否匹配,如果没有发现匹配的,则返回Effect::Pass,表示没有匹配的policy授权,那么此时需要根据其他授权机制判断请求是否执行。(其他两个状态是Effect::Allow和Effect::Deny,分别表示同意和阻止)
  3. 判断res和policy的Resource是否匹配(以及res和policy的notResource是否不匹配),如果否,返回Effect::Pass。
  4. 判断请求的操作与policy的Action是否匹配,如果否,返回Effect::Pass。
  5. 判断请求是否满足policy的所有Condition,如果满足,返回Effect::Allow,不满足,返回Effect::Deny。

其中Condition可以包括两部分的限制,一个是要求请求有指定的header项,另一个是要求请求带有指定的路径参数,在验证用户请求时,前者在rgw_build_iam_environment函数中被存入req_state::env 中;后者先被存入RGWListBucket(或RGWListBucketMultiparts等其他需要验证这些参数的对象)的成员变量中,在RGWListBucket::verify_permission()函数调用时才被存入req_state::envreq_state::env则在Condition.eval(...)中被用于比较。

有关Condition需要的参数准备过程的代码,见附录后面几个函数。

简单验证下:
创建一个名为testbucket的桶,使用s3cmd为其设置policy,发现该桶对应的bucket.instance对象的xattr中增加了相关的属性user.rgw.iam-policy,可以使用下面列出对象的所有xattr。

$ ./bin/rados -p default.rgw.meta --namespace=root listxattr .bucket.meta.testbucket:f52fe9ac-581e-432f-a8d2-363748a54fa8.4167.1

然后使用下面的命令来获得指定key的属性值,你会发现,里面存储的直接就是我们上传的json文本。

$ ./bin/rados -p default.rgw.meta --namespace=root getxattr .bucket.meta.testbucket:f52fe9ac-581e-432f-a8d2-363748a54fa8.4167.1 user.rgw.iam-policy

功能测试

基本的PUT Policy和DELETE Policy通过s3cmd测试没有问题。

下面测试了几个常用的场景用法。在测试前,先创建几个用户:
属于默认tenant(即为空)的testid 和 testid2
属于tenantone的userone和usertwo
属于tenanttwo的userthree

下面使用s3cmd测试,仅在第一个case列出完整命令,之后省略。

给所有用户授予指定权限

▸ cmh@ubuntu:~/code/files$ cat policy.json 
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": "s3:ListBucket",
            "Resource": [
                "arn:aws:s3:::bucketone",
                "arn:aws:s3:::bucketone/*"
            ],
            "Effect": "Allow",
            "Principal": {
                "AWS": "*"
            }
        }
    ]
}

▸ cmh@ubuntu:~$ cp .s3cfg_userone .s3cfg
▸ cmh@ubuntu:~/code/files$ s3cmd setpolicy policy.json s3://bucketone

▸ cmh@ubuntu:~$ cp .s3cfg_usertwo .s3cfg
▸ cmh@ubuntu:~/code/files$ s3cmd ls s3://bucketone
2017-12-07 07:53       977   s3://bucketone/objone
2017-12-07 07:53       977   s3://bucketone/objtwo

▸ cmh@ubuntu:~$ cp .s3cfg_userthree .s3cfg
▸ cmh@ubuntu:~/code/files$ s3cmd ls s3://bucketone
ERROR: Bucket 'bucketone' does not exist
ERROR: S3 error: 404 (NoSuchBucket)
▸ cmh@ubuntu:~/code/files$ s3cmd ls s3://tenantone:bucketone
2017-12-07 07:53       977   s3://tenantone:bucketone/objone
2017-12-07 07:53       977   s3://tenantone:bucketone/objtwo

给指定用户授予指定权限

▸ cmh@ubuntu:~/code/files$ cat policy.json 
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": ["s3:ListBucket","s3:GetObject"],
            "Resource": [
                "arn:aws:s3:::bucketone",
                "arn:aws:s3:::bucketone/*"
            ],
            "Effect": "Allow",
            "Principal": {
                "AWS": ["arn:aws:iam:::user/testid2","arn:aws:iam::tenanttwo:user/userthree"]
            }
        }
    ]
}

给指定用户授予所有权限

▸ cmh@ubuntu:~/code/files$ cat policy.json 
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": "s3:*",
            "Resource": [
                "arn:aws:s3:::bucketone",
                "arn:aws:s3:::bucketone/*"
            ],
            "Effect": "Allow",
            "Principal": {
                "AWS": ["arn:aws:iam:::user/testid2","arn:aws:iam::tenanttwo:user/userthree"]
            }
        }
    ]
}

给所有用户授予所有权限

▸ cmh@ubuntu:~/code/files$ cat policy.json 
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": "s3:*",
            "Resource": [
                "arn:aws:s3:::bucketone",
                "arn:aws:s3:::bucketone/*"
            ],
            "Effect": "Allow",
            "Principal": {
                "AWS":"*" 
            }
        }
    ]
}

配合Condition,给指定用户授予指定权限,并要求请求中带有指定header

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "statement1",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::tenantone:user/usertwo"
            },
            "Action": [
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::bucketone"
            ],
            "Condition": {
                "StringEquals": {
                    "aws:UserAgent": "cmh-test"
                }
            }
        },
        {
            "Sid": "statement2",
            "Effect": "Deny",
            "Principal": {
                "AWS": "arn:aws:iam::tenantone:user/usertwo"
            },
            "Action": [
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::bucketone"
            ],
            "Condition": {
                "StringNotEquals": {
                    "aws:UserAgent": "cmh-test"
                }
            }
        }
    ]
}

### 配合Condition,给指定用户授予指定权限,并要求请求带有指定路径参数

目前只支持ListBucket的s3:prefix 、 s3:delimiter 和 s3:max-keys 。

L版本验证失败,Master分支代码验证通过。

用户1设置policy

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Action": "s3:ListBucket",
            "Resource": [
                "arn:aws:s3:::bucketone",
                "arn:aws:s3:::bucketone/*"
            ],
            "Effect": "Allow",
            "Principal": {
                "AWS":"*" 
            },
            "Condition":{
                "NumericEquals": {
                    "s3:max-keys": "10"
                }
            }
        }
    ]
}

使用用户2发起请求

#!/bin/bash

access_key="usertwo123"
secret_key="usertwo123"
date=$(date -R -u)
string_to_sign="GET\n\n\n${date}\n/bucketone/"
signature=$(echo -en ${string_to_sign} | openssl sha1 -hmac ${secret_key} -binary | base64)

curl "http://127.0.0.1:8000/bucketone/?max-keys=10"              \
    -H "Date: ${date}"                                              \
    -H "User-Agent: cmh-test"                                       \
    -H "Authorization: AWS ${access_key}:${signature}"              \
    -X GET  -v

附录:代码片段

注:以下代码为master分支代码,不是L版本

RGWPutBucketPolicy::execute()

上传policy的请求执行函数

void RGWPutBucketPolicy::execute() {
  op_ret = get_params();
  if (op_ret < 0) {
    return;
  }

  bufferlist in_data = bufferlist::static_from_mem(data, len);

  if (!store->is_meta_master()) {
    op_ret = forward_request_to_master(s, NULL, store, in_data, nullptr);
    if (op_ret < 0) {
      ldout(s->cct, 20) << "forward_request_to_master returned ret=" << op_ret << dendl;
      return;
    }
  }

  try {
    Policy p(s->cct, s->bucket_tenant, in_data);
    // 将bucket原有的policy删除,将新的加入进去
    auto attrs = s->bucket_attrs;
    attrs[RGW_ATTR_IAM_POLICY].clear();
    attrs[RGW_ATTR_IAM_POLICY].append(p.text);
    op_ret = rgw_bucket_set_attrs(store, s->bucket_info, attrs,
                  &s->bucket_info.objv_tracker);
    if (op_ret == -ECANCELED) {
      op_ret = 0; /* lost a race, but it's ok because policies are immutable */
    }
  } catch (rgw::IAM::PolicyParseException& e) {
    ldout(s->cct, 20) << "failed to parse policy: " << e.what() << dendl;
    op_ret = -EINVAL;
  }
}

rgw_process_authenticated(...)

进行权限认证到执行的入口函数

int rgw_process_authenticated(RGWHandler_REST * const handler,
                              RGWOp *& op,
                              RGWRequest * const req,
                              req_state * const s,
                              const bool skip_retarget)
{
  req->log(s, "init permissions");
  
  // init_permissions 将acl、policy等信息从xattr读入内存
  // 它调用了do_init_permissions函数
  // do_init_permissions又调用了rgw_build_bucket_policies
  // rgw_build_bucket_policies的末尾部分,调用了get_iam_policy_from_attr函数
  // 将bucket policy存入了req_state.iam_policy变量中
  int ret = handler->init_permissions(op);
  if (ret < 0) {
    return ret;
  }

  /**
   * Only some accesses support website mode, and website mode does NOT apply
   * if you are using the REST endpoint either (ergo, no authenticated access)
   */
  if (! skip_retarget) {
    req->log(s, "recalculating target");
    ret = handler->retarget(op, &op);
    if (ret < 0) {
      return ret;
    }
    req->op = op;
  } else {
    req->log(s, "retargeting skipped because of SubOp mode");
  }

  /* If necessary extract object ACL and put them into req_state. */
  req->log(s, "reading permissions");
  // 该函数同样调用了get_iam_policy_from_attr函数
  // 将bucket policy存入了req_state.iam_policy变量中
  ret = handler->read_permissions(op);
  if (ret < 0) {
    return ret;
  }

  req->log(s, "init op");
  ret = op->init_processing();
  if (ret < 0) {
    return ret;
  }

  req->log(s, "verifying op mask");
  ret = op->verify_op_mask();
  if (ret < 0) {
    return ret;
  }

  req->log(s, "verifying op permissions");
  // 最终验证
  ret = op->verify_permission();
  if (ret < 0) {
    if (s->system_request) {
      dout(2) << "overriding permissions due to system operation" << dendl;
    } else if (s->auth.identity->is_admin_of(s->user->user_id)) {
      dout(2) << "overriding permissions due to admin operation" << dendl;
    } else {
      return ret;
    }
  }

  req->log(s, "verifying op params");
  ret = op->verify_params();
  if (ret < 0) {
    return ret;
  }
  // 执行具体的请求并返回结果给客户端
  req->log(s, "pre-executing");
  op->pre_exec();

  req->log(s, "executing");
  op->execute();

  req->log(s, "completing");
  op->complete();

  return 0;
}

rgw_build_iam_environment(...)

根据请求中的header,将Condition支持的header项存入req_state::env

rgw::IAM::Environment rgw_build_iam_environment(RGWRados* store,
                        struct req_state* s)
{
  rgw::IAM::Environment e;
  const auto& m = s->info.env->get_map();
  auto t = ceph::real_clock::now();
  e.emplace("aws:CurrentTime", std::to_string(ceph::real_clock::to_time_t(t)));
  e.emplace("aws:EpochTime", ceph::to_iso_8601(t));
  // TODO: This is fine for now, but once we have STS we'll need to
  // look and see. Also this won't work with the IdentityApplier
  // model, since we need to know the actual credential.
  e.emplace("aws:PrincipalType", "User");

  auto i = m.find("HTTP_REFERER");
  if (i != m.end()) {
    e.emplace("aws:Referer", i->second);
  }

  // These seem to be the semantics, judging from rest_rgw_s3.cc
  i = m.find("SERVER_PORT_SECURE");
  if (i != m.end()) {
    e.emplace("aws:SecureTransport", "true");
  }

  i = m.find("HTTP_HOST");
  if (i != m.end()) {
    e.emplace("aws:SourceIp", i->second);
  }

  i = m.find("HTTP_USER_AGENT"); {
  if (i != m.end())
    e.emplace("aws:UserAgent", i->second);
  }

  if (s->user) {
    // What to do about aws::userid? One can have multiple access
    // keys so that isn't really suitable. Do we have a durable
    // identifier that can persist through name changes?
    e.emplace("aws:username", s->user->user_id.id);
  }
  return e;
}

RGWListBucket_ObjStore_S3::get_params()

从请求中解析出prefix、marker、max_keys、delimiter等参数,存入RGWListBucket的成员变量中。

int RGWListBucket_ObjStore_S3::get_params()
{
  list_versions = s->info.args.exists("versions");
  prefix = s->info.args.get("prefix");
  if (!list_versions) {
    marker = s->info.args.get("marker");
  } else {
    marker.name = s->info.args.get("key-marker");
    marker.instance = s->info.args.get("version-id-marker");
  }
  max_keys = s->info.args.get("max-keys");
  op_ret = parse_max_keys();
  if (op_ret < 0) {
    return op_ret;
  }
  delimiter = s->info.args.get("delimiter");
  encoding_type = s->info.args.get("encoding-type");
  if (s->system_request) {
    s->info.args.get_bool("objs-container", &objs_container, false);
    const char *shard_id_str = s->info.env->get("HTTP_RGWX_SHARD_ID");
    if (shard_id_str) {
      string err;
      shard_id = strict_strtol(shard_id_str, 10, &err);
      if (!err.empty()) {
        ldout(s->cct, 5) << "bad shard id specified: " << shard_id_str << dendl;
        return -EINVAL;
      }
    } else {
      shard_id = s->bucket_instance_shard_id;
    }
  }
  return 0;
}

RGWListBucket::verify_permission()

将RGWListBucket成员变量中的prefix、delimiter、max-keys三者被Condition的参数,存入req_state::env中,用于之后的Condition::eval()

int RGWListBucket::verify_permission()
{
  op_ret = get_params();
  if (op_ret < 0) {
    return op_ret;
  }
  if (!prefix.empty())
    s->env.emplace("s3:prefix", prefix);

  if (!delimiter.empty())
    s->env.emplace("s3:delimiter", delimiter);

  s->env.emplace("s3:max-keys", std::to_string(max));

  if (!verify_bucket_permission(s,
                list_versions ?
                rgw::IAM::s3ListBucketVersions :
                rgw::IAM::s3ListBucket)) {
    return -EACCES;
  }

  return 0;
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,076评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,658评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,732评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,493评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,591评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,598评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,601评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,348评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,797评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,114评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,278评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,953评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,585评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,202评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,180评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,139评论 2 352

推荐阅读更多精彩内容