Ceph

ceph rgw:bucket policy实现

2018-02-06  本文已影响133人  chnmagnus

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

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

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

实现

为一个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;
}
上一篇 下一篇

猜你喜欢

热点阅读