亚马逊AWS官方博客

多云身份管控平台构建——第二篇 基于动态策略实现Auth0与亚马逊云控制台的权限集成

越来越多的企业在迁移上云旅程中都会面临多云的选择。不管是出于主动原因希望融合多云的服务优势,还是出于被动原因需要适应公司的业务并购,抑或是因为上云过程中需要实现混合云架构的打通,构建多云的统一身份权限管控平台往往是这些企业需要解决的问题。

构建这一平台,不仅要实现单点登录,还要考虑统一身份管控、统一权限管控和云上用户行为审计;并且企业内人员的多组织管理模型通常与云上基于角色的IAM管理模型存在较大差异,这进一步增加了构建跨云身份管理平台的难度。

本博客从企业客户的实际需求和技术难点出发,分别阐述了使用

  • RBAC — 基于角色的权限管控
  • ABAC — 基于属性的权限管控
  • 动态策略生成

这三种思路实现企业统一身份管控平台,并与公有云控制台(Web console)实现身份和权限的打通;以及三种思路各自的适用场景;并在此基础上给出了基于Auth0构建该平台,完成与亚马逊云控制台集成,实现统一登录与授权的具体实现代码样例。

受篇幅所限,我们把博客分成两部分,上一篇讲述RBAC和ABAC的实现方法;本篇主要讲述动态策略生成的实现方法。

多云身份管控平台构建——第一篇 基于RBAC&ABAC实现Auth0与亚马逊云控制台的权限集成

1. 方案概述

在第一篇,我们讲述的RBAC思路用于实现IDP中的用户属性(项目与角色的组合)与Amazon IAM角色的一对一映射。集成起来简单快速,适合用户的项目x角色组合数量较少的使用场景;而ABAC则适用于多租户模式下对资源类型使用相对固定的场景。我们使用一个policy,通过资源标签和主体标签的匹配规则就可以覆盖所有权限管控需求,并可以扩展至任意数量的项目

但实际的企业环境中,如果项目的数量很多,而且每个项目中用到的资源类型又不尽相同,RBAC和APBAC都很难满足需求。为了提升环境的可管理性和敏捷性,避免可能产生的策略数量和尺寸的爆炸性增长,基于动态策略生成的权限管理模型将是一个更为通用的解决方案。基本思路如下图:

  • 首先,构建策略模版库,用于保存各种云资源的动态权限模版
    模版库可以保存在DynamoDB或S3中。为便于后续的维护和管理,建议对每个资源的不同角色分别维护一个独立的模版。下面的策略模版库演示了允许查询和启动EC2实例的一个策略模版,其中{{region}}、{{accountid}}和{{project}}都为模版中的placeholder,以备后续动态生成策略。
 	{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ec2:Describe*",
                "ec2:List*"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "ec2:StartInstances"
            ],
            "Resource": "arn:aws:ec2:{{region}}:{{accountid}}:instance/*",
            "Condition": {
                "StringEquals": {
                    "aws:ResourceTag/environment": "{{project}}"
                }
            }
        }
    ]
}
  • 之后,维护用户属性与策略模版间的对应关系
    在实际项目中,该对应关系可以通过开发配置界面完成,便于管理员进行动态配置。
项目 角色 模版列表
Project1 Readonly EC2-ReadOnly-template
Project1 Readonly S3-ReadOnly-Bucket-template
Project1 Readonly DDB-ReadOnly-Table-template
Project1 Manager EC2-AllAccess-template
  •  最后,构建策略生成器,用于根据用户登录后的上下文信息动态生成IAM策略
    用户在IDP完成身份认证后,通过OpenID或SAML断言携带用户的上下文信息。我们构建的策略动态生成器需要先验证用户的合法身份,然后根据上下文信息获取对应的策略模版,并生成所需的策略,之后通过Amazon STS服务完成AssumeRole,获得临时的AKSK、SessionToken等信息,生成访问亚马逊云控制台的URL,以保证资源访问的隔离。

这一方案的好处显而易见,它不拘于使用RBAC或ABAC模型,也不依赖于标签的使用,我们可以在模版中任何需要的地方放置placeholder,构建所需策略,非常灵活。

下面我们对几个技术关键点的实现做详细展开:

2. 配置Auth0实现基于OIDC的身份认证

本博客,我们使用Auth0作为统一身份管控平台的IDP。验证中,我们使用了OIDC的身份层协议,在Auth0完成身份认证后将向管控平台返回IDToken和Access Token。

2.1 IDToken or AccessToken?

当前的场景是企业的身份管控平台需要从Auth0获得IDToken,表明已完成身份认证,具体的资源授权由API GW和后端的Lambda根据Token中携带的用户属性自行完成,因此应向后端的APIGateway传递IDtoken。在其他的场景,如果是访问Auth0获得对资源的访问授权,携带允许访问资源的scope信息到resource server获取相应资源的场景(如常见的使用微信认证,并允许获得微信头像的场景),则应使用access token。因此在后面的方案展示中,我们将使把从Auth0中获得的IDToken传递给APIGateway

2.2 配置Auth0获取IDToken

在这个场景中,企业的身份管控平台需要依赖Auth0完成身份认证,再将从Auth0获得的包含用户属性信息的ID token携带在http header中,请求API Gateway调用。在Auth0中我们需要完成如下的配置,以获得包含完整用户属性信息的ID token:

  • 首先,在Auth0中创建API application
    创建成功后,将获得application对应的clientID和client secret。clientID后面会包含在ID token的aud中,用于表明该token是合法颁发给该application的。
  • 然后,在创建的application中选择认证使用的工作流
    在application ->新创建的Application ->Advanced Settings中选择Implicit的授权模式。该模式为0中的RFC6749中的隐式授权模式,授权过程安全且较为简单,不需授权码可直接获得IDToken。

  • 最后,修改application的登录流程,加入客户化的action“appProject2Token”,该Action用于把用户的user_metadata中的project和role的属性信息加入到IDToken中

  • Action代码如下:
exports.onExecutePostLogin = async (event, api) => {
  const namespace = 'https://mytesttest.com';
  const project = event.user.user_metadata.project;
  const role = event.user.user_metadata.role;

  //if (event.authorization) { 这里为简便起见忽略了验证部分,实际项目中可根据安全需要对applications ->api -> permission中限定的scope进行验证,允许访问相应的内容时,才将用户的相关信息放入IDToken
    // Set claims 
    api.idToken.setCustomClaim(`${namespace}/project`, event.user.user_metadata.project);
    api.idToken.setCustomClaim(`${namespace}/role`, event.user.user_metadata.role);
  //}
};

2.3 IDToken获取验证

下面我们验证从Auth0中获取的IDToken

Auth0提供了Authentication API Debugger可以方便地查看获取到的IDToken。对创建的API application使用如下的authorizer接口进行访问(下面的{{}}部分用API application产生的domain和clientid替换),rediect_url为Authentication API Debugger的URL。

https://{{ApplicationDomain}}/authorize?responsetype=idtoken token&responsemode=formpost&clientid={{CLINTID}}&redirecturi=https://dev-gzu7esjn.us.webtask.run/auth0-authentication-api-debugger&scope=openid profile&state=test&nonce=test&audience=https://oauth0api.com

可以看到返回的IDToken部分包含我们之前定义的用户属性信息(project和role)。

3.动态策略生成器实现

企业身份管控平台在Auth0完成身份验证并获得IDToken后,我们使用Amazon API Gateway的Lambda Authorizer构建策略生成器,由于请求处理和策略生成。Lambda Authorizer是API Gateway的一个功能,该功能使用 Lambda 函数来控制对 API Gateway 的访问。在该Lambda函数中我们主要完成了两项工作:首先,根据持有者令牌(如 OAuth 或 SAML)验证用户身份的合法性,验证通过即可允许API Gateway的调用;同时对合法用户,根据用户的上下文信息结合策略模版动态生成访问策略,并将生成的策略传递给后端的业务Lambda,业务Lambda根据这些policy通过AssumeRole获得临时凭证来生成Amazon console URL;也可以构建业务Lambda通过临时凭证完成对后端资源的安全访问。

3.1构建具有Authorizer Lambda的APIGateway

实现中,我们构建了名为“auth0api”的Rest APIGateway,调用后端名为“mytest”的业务Lambda,业务Lambda生成访问亚马逊云控制台的URL通过APIGateway返回给前端的client app/browser

下图展示了为该APIGateway添加Authorizer Lambda,选择Token作为Lambda Event负载,选择认证后的结果信息在APIGateway中保留3s,期间不需要再次通过Authorizer 做认证。

3.2 Authorizer Lambda的实现

该函数是动态策略生成器的核心部分。它负责获取APIGateway中的IDToken信息,校验IDToken的合法性。

完整的代码可以参考https://github.com/1559550282/AWS/tree/main/MultiCloud-identityPrivControl

def lambda_handler(event, context):
    #从Event中获取APIGateway中传来的IDToken
    token = event['authorizationToken'].split(" ")
    if (token[0] != 'Bearer'):
        raise Exception('Authorization header should have a format Bearer <JWT> Token')
    jwt_bearer_token = token[1]  
    unauthorized_claims = jwt.get_unverified_claims(jwt_bearer_token) 
    #并从IDToken中获得project和role的属性
    project_attri = unauthorized_claims['https://mytesttest.com/project']
    accessrole_attri = unauthorized_claims['https://mytesttest.com/role']
    
    #从Auth0中创建的API Application中获得jwt签名的公共密钥
    keys_url = 'https://dev-gzu7esjn.us.auth0.com/.well-known/jwks.json' 
    with urllib.request.urlopen(keys_url) as f:
        response = f.read()
    keys = json.loads(response.decode('utf-8'))['keys']

    #用公共密钥校验IDToen中的签名部分,并核对IDToken中的aud是否为Auth0中创建的API Application对应的ClientID,是则认为用户校验通过。该部分代码篇幅原因删略,如需要请参见github链接
    response = validateJWT(jwt_bearer_token, appclientid, keys)
    
    #get authenticated claims
    if (response == False):
        raise Exception('Unauthorized')
    else:
        principal_id = response["sub"]
    
    #生成APIGateway的policy--authResponse,通过Authorizer lambda 返回给apigw,决定是否允许调用APIGateway
    tmp = event['methodArn'].split(':')
    api_gateway_arn_tmp = tmp[5].split('/')
    aws_account_id = tmp[4]  
    policy = AuthPolicy(principal_id, aws_account_id)
    policy.restApiId = api_gateway_arn_tmp[0]
    policy.region = tmp[3]
    policy.stage = api_gateway_arn_tmp[1]
    policy.allowAllMethods() 
    authResponse = policy.build()
    
    #同时,获取IDToken中的用户属性信息,结合策略模版为后端的业务lambda生成动态policy:deliverpolicy,通过上下文传给后端业务lambda
    deliverpolicy = policyGenerator(project_attri, accessrole_attri)
    ##将生成的动态policy向后端业务传递
    context = {
        'policy': deliverpolicy
    }
    authResponse['context'] = context
    return authResponse
    
## 根据用户的属性信息从DynamoDB中取出策略模版,使用策略模版动态生成policy。模版替换使用mastache的python版本实现。    
def policyGenerator(project, projrole):
    dynamodb = boto3.client('dynamodb')
    response = dynamodb.get_item(
        TableName="policytemplate",
        Key={
        'project': {'S': 'project'},
        'role': {'S': 'projrole'}
        }
    )
    thecontext = {'region':REGIONID,'accountid':ACCOUNTID,'project':project}
    temppolicy = pystache.render(response['Item']['policy']['S'],thecontext)
    return temppolicy

3.3 构建业务Lambda生成符合权限管控的亚马逊云控制台

Athorizer Lambda生成了APIGateway的访问策略和后端业务所需的动态策略,返回给APIGateway。后端业务Lambda与APIGateway的集成应注意使用“Lambda proxy Integration”,以便业务Lambda可以从event参数中获取APIGateway传递的从Athorizer Lambda获得的动态policy

业务Lambda生成亚马逊云控制台 URL的代码片段:

def lambda_handler(event, context):
  ##由于Amazon console不支持角色串联(参见https://aws.amazon.com/cn/premiumsupport/knowledge-center/iam-role-chaining-limit/),因此示例中使用了IAM user的AKSK再通过assumerole的方式挂载policy
    session = boto3.session.Session(aws_access_key_id="xxxxx", aws_secret_access_key="xxxx")
    sts_connection = session.client('sts')
    ##在APIGateway传递来的event中获取由authorizer lambda生成的动态policy
    thepolicy = event['requestContext']['authorizer']['policy']
    
    ##由于assume_role最终生成的临时凭证是基础role_arn和policy的合集,因此示例中选用的较大权限。实际生产中可以根据需要选择合理范围。同时注意对role_arn创建对指定aksk的用户的信任策略,以便assumerole具有权限。
    role_arn = "arn:aws:iam::{}:role/adminrole".format(aws_account_id)
    assumed_role = sts_connection.assume_role(
        RoleArn=role_arn,
        RoleSessionName="console-session",
        Policy=thepolicy
    )
    credentials = assumed_role["Credentials"]
    # 获得临时凭证的AKSK和session Token
    url_credentials = {}
    url_credentials['sessionId'] = credentials['AccessKeyId']
    url_credentials['sessionKey'] = credentials['SecretAccessKey']
    url_credentials['sessionToken'] = credentials["SessionToken"]
    json_string_with_temp_credentials = json.dumps(url_credentials)
    
    #用临时凭证构建Amazon console
    request_parameters = "?Action=getSigninToken"
    request_parameters += "&SessionDuration=43200"
    if sys.version_info[0] < 3:
        def quote_plus_function(s):
            return urllib.quote_plus(s)
    else:
        def quote_plus_function(s):
            return urllib.parse.quote_plus(s)
    
    request_parameters += "&Session=" + quote_plus_function(json_string_with_temp_credentials)
    request_url = "https://signin.aws.amazon.com/federation" + request_parameters
    r = requests.get(request_url)
    signin_token = json.loads(r.text)

    request_parameters = "?Action=login"
    request_parameters += "&Issuer=Example.org"
    request_parameters += "&Destination=" + quote_plus_function("https://console.aws.amazon.com/")
    request_parameters += "&SigninToken=" + signin_token["SigninToken"]
    request_url = "https://signin.aws.amazon.com/federation" + request_parameters
    
    return {
        'statusCode': 200,
        'body': json.dumps(request_url)
    }

4. 验证

使用2.3节获得的IDToken调用APIGateway

curl –request GET –url https://{{APIID}}.execute-api.ap-southeast-1.amazonaws.com/default/mytest –header ‘authorization: Bearer {{IDToken}}’

即可获得访问Amazon console的url如下:

https://signin.aws.amazon.com/federation?Action=login&Issuer=Example.org&Destination=https%3A%2F%2Fconsole.aws.amazon.com%2F&SigninToken=xxxx

该URL的有效期可以在assume_role中设定从15min到12小时的时长。

总结

至此,我们介绍了基于RBAC、ABAC和动态策略生成这三种方式实现统一身份认证平台,完成用户属性与IAM角色的对应,从而构建统一身份管控平台与Amazon控制台的SSO和统一授权。

实际场景中,如果用户多种属性的组合数量非常有限,推荐使用RBAC实现属性与IAM角色的直接映射,简单快捷;如果不同属性的用户使用的资源类型整齐划一,则可以使用ABAC通过会话标签和资源标签实现IAM角色的限定;但如果用户多种属性的组合过于复杂,而且不同类型的用户使用的资源需求不尽相同,则可以使用动态策略生成的方式,通过策略模版生成动态策略,实现灵活的权限隔离。

本篇作者

倪惠青

AWS 解决方案架构师,负责基于AWS云计算方案架构的咨询和设计,在国内推广AWS云平台技术和各种解决方案。在加入AWS 之前曾在Oracle,Microsoft工作多年,负责企业公有云方案咨询和架构设计,在基础架构及大数据方面有丰富经验。

任耀洲

AWS解决方案架构师,负责企业客户应用在AWS的架构咨询和设计。在微服务架构设计、数据库等领域有丰富的经验