요청 및 응답 보안을 위한 액세스 제어 사용 사례 - AWS AppSync

기계 번역으로 제공되는 번역입니다. 제공된 번역과 원본 영어의 내용이 상충하는 경우에는 영어 버전이 우선합니다.

요청 및 응답 보안을 위한 액세스 제어 사용 사례

보안 섹션에서는 사용자 보호를 위한 다양한 인증 모드에 대해 알아보고 API 개념과 흐름을 이해할 수 있도록 세분화된 인증 메커니즘을 소개했습니다. GraphQL Resolver Mapping 템플릿을 사용하여 데이터에 대한 로직 풀 연산을 수행할 수 있으므로 사용자 ID, 조건부 및 데이터 주입의 조합을 사용하여 매우 유연한 방식으로 읽기 또는 쓰기 시 데이터를 보호할 수 있습니다. AWS AppSync

AWS AppSync 리졸버 편집에 익숙하지 않은 경우 프로그래밍 가이드를 검토하세요.

개요

지금까지는 시스템의 데이터에 대한 액세스 권한 부여가 액세스 제어 매트릭스를 통해 수행되었으며, 이 매트릭스에서 행(리소스)과 열(사용자/역할)의 교차점이 부여되는 권한이었습니다.

AWS AppSync 사용자 계정의 리소스를 사용하고 ID (사용자/역할) 정보를 GraphQL 요청 및 응답에 컨텍스트 객체로 스레드합니다. 이 객체는 리졸버에서 사용할 수 있습니다. 다시 말해서 해석기 로직에 따라 쓰기 또는 읽기 작업에 대한 권한을 적절히 부여할 수 있습니다. 예를 들어 이름이 지정된 특정 사용자 또는 그룹만 특정 데이터베이스 행을 읽고 쓸 수 있는 경우와 같이 이 로직이 리소스 수준인 경우 해당 “권한 부여 메타데이터”를 저장해야 합니다. AWS AppSync 데이터를 저장하지 않으므로 권한을 계산할 수 있으려면 이 권한 부여 메타데이터를 리소스와 함께 저장해야 합니다. 권한 부여 메타데이터는 일반적으로 소유자 또는 사용자/그룹 목록 같은 DynamoDB 테이블에 속성(열)입니다. 예를 들면 ReadersWriters 속성이 있을 수 있습니다.

다시 말해, 상위 수준에서는 데이터 원본의 개별 항목을 읽으려는 경우 해석기를 데이터 원본에서 읽어온 후 응답 템플릿에서 조건부 #if () ... #end 문을 수행할 수 있다는 뜻입니다. 이 확인은 일반적으로 읽기 작업에서 반환되는 권한 부여 메타데이터와 비교하여 멤버십을 확인하기 위해 $context.identity의 사용자 또는 그룹 값을 사용합니다. Scan 또는 Query 테이블에서 반환되는 목록과 같이 여러 레코드의 경우, 유사한 사용자 또는 그룹 값을 사용하여 작업의 일부로서 조건부 확인을 데이터 원본으로 전송합니다.

데이터를 작성할 때와 마찬가지로 조건문을 작업(예: PutItem 또는 UpdateItem)에 적용하여 변형을 만드는 사용자나 그룹이 권한을 가지고 있는지 여부를 확인합니다. 이 조건문은 $context.identity의 값 하나를 여러 번 다시 사용하여 해당 리소스에 대한 권한 부여 메타데이터와 비교합니다. 요청 및 응답 템플릿 모두에 대해 클라이언트의 사용자 지정 헤더를 사용하여 검증 확인을 수행할 수도 있습니다.

데이터 읽기

위에 요약된 바와 같이 확인을 수행할 권한 부여 메타데이터를 리소스와 함께 저장하거나 GraphQL 요청(자격 증명, 헤더 등)에 전달해야 합니다. 이를 설명하기 위해 아래에 나오는 DynamoDB 테이블이 있다고 가정하겠습니다.

DynamoDB table with ID, Data, PeopleCanAccess, GroupsCanAccess, and Owner columns.

기본 키는 id이며 액세스해야 할 데이터는 Data입니다. 다른 열은 승인을 위해 수행할 수 있는 확인의 예입니다. DynamoDB에 대한 해석기 매핑 템플릿 참조에 설명된 대로 OwnerString이고 PeopleCanAccessGroupsCanAccessString Sets입니다.

해석기 매핑 템플릿 개요의 다이어그램은 응답 템플릿에 컨텍스트 객체는 물론 데이터 원본의 결과도 포함됨을 보여줍니다. 개별 항목의 GraphQL 쿼리의 경우 응답 템플릿을 사용하여 사용자에게 이러한 결과를 보도록 허용되었는지 여부를 확인하거나 아니면 권한 부여 오류 메시지를 반환할 수 있습니다. 이를 때로는 "권한 부여 필터"라고도 합니다. 스캔 또는 쿼리를 사용하는 GraphQL 쿼리 반환 목록의 경우, 요청 템플릿에 대한 확인을 수행하고 권한 부여 조건이 충족되는 경우에만 데이터를 반환하는 것이 더 바람직합니다. 구현은 다음과 같습니다.

  1. GetItem - 개별 레코드에 대한 권한 확인. #if() ... #end 문을 사용하여 수행됩니다.

  2. 스캔/쿼리 작업 - 권한 부여 확인이 "filter":{"expression":...} 문입니다. 일반적으로 같음(attribute = :input)을 확인하거나 목록에 값(contains(attribute, :input))이 있는지 여부를 확인합니다.

#2에서는 두 명령문의 attribute가 위의 예제의 Owner와 같은 테이블 내 레코드 열 이름을 나타냅니다. # 기호를 사용하여 이 이름을 별칭으로 지정하고 "expressionNames":{...}를 사용할 수 있지만, 필수는 아닙니다. :input은 데이터베이스 속성과 비교할 값에 대한 참조로, "expressionValues":{...}에서 정의합니다. 다음과 같은 예제가 표시됩니다.

사용 사례: 소유자에게 읽기 허용

위의 테이블을 사용할 때, 개별 읽기 작업(Owner == Nadia)에 대해 GetItem일 때만 데이터를 반환하려는 경우 템플릿은 다음과 같습니다.

#if($context.result["Owner"] == $context.identity.username) $utils.toJson($context.result) #else $utils.unauthorized() #end

여기에 언급된 몇 가지 사항은 나머지 단원에서 재사용될 것입니다. 먼저, 검사 시 Amazon Cognito 사용자 풀을 사용하는 경우 친숙한 사용자 가입 이름을 사용하고 사용하는 IAM 경우 사용자 ID (Amazon Cognito 페더레이션 ID 포함) 를 사용합니다$context.identity.username. 고유한 'Amazon Cognito 자격 증명' 값(여러 위치에서 로그인을 연동할 때 유용함)과 같이 소유자에 대해 저장할 다른 값도 있으며, 해석기 매핑 템플릿 컨텍스트 참조에서 사용 가능한 옵션을 검토해야 합니다.

둘째, 조건부 else check $util.unauthorized() 응답은 완전히 선택 사항이지만 API GraphQL을 설계할 때 모범 사례로 사용하는 것이 좋습니다.

사용 사례: 특정 액세스 하드코딩

// This checks if the user is part of the Admin group and makes the call #foreach($group in $context.identity.claims.get("cognito:groups")) #if($group == "Admin") #set($inCognitoGroup = true) #end #end #if($inCognitoGroup) { "version" : "2017-02-28", "operation" : "UpdateItem", "key" : { "id" : $util.dynamodb.toDynamoDBJson($ctx.args.id) }, "attributeValues" : { "owner" : $util.dynamodb.toDynamoDBJson($context.identity.username) #foreach( $entry in $context.arguments.entrySet() ) ,"${entry.key}" : $util.dynamodb.toDynamoDBJson($entry.value) #end } } #else $utils.unauthorized() #end

사용 사례: 결과 목록 필터링

이전 예에서는 단일 항목을 반환하므로 $context.result에 대해 확인을 직접 수행할 수 있었지만, 스캔과 같은 일부 작업은 $context.result.items에서 여러 항목을 반환합니다. 이 경우에는 권한 부여 필터를 수행하여 사용자에게 보기가 허용된 결과만 반환해야 합니다. Owner 필드에서 현재 Amazon Cognito IdentityID가 레코드에 대해 설정되었다고 가정해 보겠습니다. 이제 다음 응답 매핑 템플릿을 사용하여 사용자가 소유하는 레코드만 표시하도록 필터링할 수 있습니다.

#set($myResults = []) #foreach($item in $context.result.items) ##For userpools use $context.identity.username instead #if($item.Owner == $context.identity.cognitoIdentityId) #set($added = $myResults.add($item)) #end #end $utils.toJson($myResults)

사용 사례: 여러 사람에게 읽기 허용

또 한 가지 널리 사용되는 권한 부여 옵션은 사람 그룹에 데이터 읽기를 허용하는 것입니다. 아래 예에서는 GraphQL 쿼리를 실행하는 사용자가 "filter":{"expression":...} 세트에 나열되는 경우에만 PeopleCanAccess가 테이블 스캔에서 값을 반환합니다.

{ "version" : "2017-02-28", "operation" : "Scan", "limit": #if(${context.arguments.count}) $util.toJson($context.arguments.count) #else 20 #end, "nextToken": #if(${context.arguments.nextToken}) $util.toJson($context.arguments.nextToken) #else null #end, "filter":{ "expression": "contains(#peopleCanAccess, :value)", "expressionNames": { "#peopleCanAccess": "peopleCanAccess" }, "expressionValues": { ":value": $util.dynamodb.toDynamoDBJson($context.identity.username) } } }

사용 사례: 그룹에 읽기 허용

마지막 사용 사례와 마찬가지로, 이 사용 사례에서는 하나 이상의 그룹 내 사람들만 데이터베이스의 특정 항목을 읽을 수 있습니다. "expression": "contains()" 작업의 사용도 비슷하지만, 이것은 설정된 멤버십에서 사용자가 속해 있을 수 있는 모든 그룹의 논리 OR입니다. 여기서는 사용자가 속한 각 그룹에 대해 아래 $expression 문을 작성한 다음 이 문을 필터에 전달합니다.

#set($expression = "") #set($expressionValues = {}) #foreach($group in $context.identity.claims.get("cognito:groups")) #set( $expression = "${expression} contains(groupsCanAccess, :var$foreach.count )" ) #set( $val = {}) #set( $test = $val.put("S", $group)) #set( $values = $expressionValues.put(":var$foreach.count", $val)) #if ( $foreach.hasNext ) #set( $expression = "${expression} OR" ) #end #end { "version" : "2017-02-28", "operation" : "Scan", "limit": #if(${context.arguments.count}) $util.toJson($context.arguments.count) #else 20 #end, "nextToken": #if(${context.arguments.nextToken}) $util.toJson($context.arguments.nextToken) #else null #end, "filter":{ "expression": "$expression", "expressionValues": $utils.toJson($expressionValues) } }

데이터 쓰기

변형에 대한 데이터 쓰기는 항상 요청 매핑 템플릿에서 제어됩니다. DynamoDB 데이터 원본의 경우, 핵심은 해당 테이블의 권한 부여 메타데이터를 확인하는 적정 "condition":{"expression"...}"을 사용하는 것입니다. 보안 단원에 테이블의 Author 필드를 확인할 수 있는 예제를 제공했습니다. 이 단원의 사용 사례에서는 더 많은 사용 사례를 살펴봅니다.

사용 사례: 여러 소유자

앞의 예제 테이블 다이어그램을 사용하면서, PeopleCanAccess 목록을 가정해 보겠습니다.

{ "version" : "2017-02-28", "operation" : "UpdateItem", "key" : { "id" : $util.dynamodb.toDynamoDBJson($ctx.args.id) }, "update" : { "expression" : "SET meta = :meta", "expressionValues": { ":meta" : $util.dynamodb.toDynamoDBJson($ctx.args.meta) } }, "condition" : { "expression" : "contains(Owner,:expectedOwner)", "expressionValues" : { ":expectedOwner" : $util.dynamodb.toDynamoDBJson($context.identity.username) } } }

사용 사례: 그룹에 새 레코드 생성 허용

#set($expression = "") #set($expressionValues = {}) #foreach($group in $context.identity.claims.get("cognito:groups")) #set( $expression = "${expression} contains(groupsCanAccess, :var$foreach.count )" ) #set( $val = {}) #set( $test = $val.put("S", $group)) #set( $values = $expressionValues.put(":var$foreach.count", $val)) #if ( $foreach.hasNext ) #set( $expression = "${expression} OR" ) #end #end { "version" : "2017-02-28", "operation" : "PutItem", "key" : { ## If your table's hash key is not named 'id', update it here. ** "id" : $util.dynamodb.toDynamoDBJson($ctx.args.id) ## If your table has a sort key, add it as an item here. ** }, "attributeValues" : { ## Add an item for each field you would like to store to Amazon DynamoDB. ** "title" : $util.dynamodb.toDynamoDBJson($ctx.args.title), "content": $util.dynamodb.toDynamoDBJson($ctx.args.content), "owner": $util.dynamodb.toDynamoDBJson($context.identity.username) }, "condition" : { "expression": $util.toJson("attribute_not_exists(id) AND $expression"), "expressionValues": $utils.toJson($expressionValues) } }

사용 사례: 그룹에 기존 레코드 업데이트 허용

#set($expression = "") #set($expressionValues = {}) #foreach($group in $context.identity.claims.get("cognito:groups")) #set( $expression = "${expression} contains(groupsCanAccess, :var$foreach.count )" ) #set( $val = {}) #set( $test = $val.put("S", $group)) #set( $values = $expressionValues.put(":var$foreach.count", $val)) #if ( $foreach.hasNext ) #set( $expression = "${expression} OR" ) #end #end { "version" : "2017-02-28", "operation" : "UpdateItem", "key" : { "id" : $util.dynamodb.toDynamoDBJson($ctx.args.id) }, "update":{ "expression" : "SET title = :title, content = :content", "expressionValues": { ":title" : $util.dynamodb.toDynamoDBJson($ctx.args.title), ":content" : $util.dynamodb.toDynamoDBJson($ctx.args.content) } }, "condition" : { "expression": $util.toJson($expression), "expressionValues": $utils.toJson($expressionValues) } }

퍼블릭 및 프라이빗 레코드

또한 조건부 필터를 사용하여 데이터를 프라이빗, 퍼블릭 또는 다른 부울 확인으로 표시하도록 선택할 수도 있습니다. 이렇게 하면 조건 필터를 응답 템플릿 내부에서 권한 부여 필터의 일부로서 결합할 수 있습니다. 이 확인을 사용하는 것은 그룹 멤버십을 제어하지 않고 데이터를 임시로 숨기거나 뷰에서 제거할 수 있는 좋은 방법입니다.

예를 들어 DynamoDB 테이블의 각 항목에 대해 yes 또는 no 값을 사용하여 public이라는 속성을 추가했다고 가정하겠습니다. 다음 응답 템플릿은 사용자가 액세스 AND 권한이 있는 그룹에 속해 있는 경우 해당 데이터가 공개로 표시된 경우에만 데이터를 표시하도록 GetItem 호출에 사용할 수 있습니다.

#set($permissions = $context.result.GroupsCanAccess) #set($claimPermissions = $context.identity.claims.get("cognito:groups")) #foreach($per in $permissions) #foreach($cgroups in $claimPermissions) #if($cgroups == $per) #set($hasPermission = true) #end #end #end #if($hasPermission && $context.result.public == 'yes') $utils.toJson($context.result) #else $utils.unauthorized() #end

또한 위의 코드는 OR(||) 로직을 사용하여 퍼블릭 레코드인 경우 또는 레코드에 대한 권한이 있는 사람에게만 읽기를 허용할 수도 있습니다.

#if($hasPermission || $context.result.public == 'yes') $utils.toJson($context.result) #else $utils.unauthorized() #end

일반적으로 표준 연산자인 ==, !=, &&||는 권한 부여 확인 수행 시 유용합니다.

실시간 데이터

이 설명서의 앞부분에 설명된 동일한 기술을 사용하여 클라이언트가 구독을 만들 때 세분화된 액세스 제어를 GraphQL 구독에 적용할 수 있습니다. 구독 필드에 해석기를 연결할 수 있으며, 이때 데이터 원본에서 데이터를 쿼리하고 요청이나 응답 매핑 템플릿에서 조건부 로직을 수행할 수 있습니다. 또한 데이터 구조가 GraphQL 구독의 반환 유형과 일치하는 한, 구독의 초기 결과 등 추가 데이터를 클라이언트로 반환할 수도 있습니다.

사용 사례: 사용자가 특정 대화만 구독 가능

GrapHQL 구독과 관련된 실시간 데이터의 일반 사용 사례 중 하나는 메시징 또는 프라이빗 채팅 애플리케이션 빌드입니다. 여러 사용자가 있는 채팅 애플리케이션을 작성하려는 경우 두 사람 이상의 여러 사람 간에 대화가 발생할 수 있습니다. 이러한 대화는 프라이빗이나 퍼블릭 "대화방"으로 그룹화될 수 있습니다. 따라서, 자신에게 액세스 권한이 부여된 대화(한 개 이상의 그룹일 수 있음)만 구독하도록 사용자에게 권한을 부여할 수 있습니다. 데모 목적으로 제공되는 아래 샘플은 한 사용자가 프라이빗 메시지를 다른 사용자에게 전송하는 단순 사용 사례를 보여줍니다. 설정에는 두 개의 Amazon DynamoDB 테이블이 있습니다.

  • Messages 테이블: (기본 키)toUser, (정렬 키)id

  • Permissions 테이블: (기본 키)username

Messages 테이블은 GraphQL 변형을 통해 전송되는 실제 메시지를 저장합니다. Permissions 테이블은 클라이언트 연결 시 권한 부여를 위해 GraphQL 구독에서 확인됩니다. 아래 예제에서는 다음과 같은 GraphQL 스키마를 사용한다고 가정합니다.

input CreateUserPermissionsInput { user: String! isAuthorizedForSubscriptions: Boolean } type Message { id: ID toUser: String fromUser: String content: String } type MessageConnection { items: [Message] nextToken: String } type Mutation { sendMessage(toUser: String!, content: String!): Message createUserPermissions(input: CreateUserPermissionsInput!): UserPermissions updateUserPermissions(input: UpdateUserPermissionInput!): UserPermissions } type Query { getMyMessages(first: Int, after: String): MessageConnection getUserPermissions(user: String!): UserPermissions } type Subscription { newMessage(toUser: String!): Message @aws_subscribe(mutations: ["sendMessage"]) } input UpdateUserPermissionInput { user: String! isAuthorizedForSubscriptions: Boolean } type UserPermissions { user: String isAuthorizedForSubscriptions: Boolean } schema { query: Query mutation: Mutation subscription: Subscription }

createUserPermissions()와 같은 표준 작업 중 일부는 구독 해석기를 보여주기 위해 아래에 포함되지 않지만, DynamoDB 해석기의 표준 구현입니다. 대신에 해석기를 사용하는 구독 권한 부여 흐름을 중점적으로 살펴보겠습니다. 한 사용자에서 다른 사용자로 메시지를 전송하려면 해석기를 sendMessage() 필드에 연결하고 다음과 같은 요청 템플릿과 함께 Messages 테이블 데이터 원본을 선택합니다.

{ "version" : "2017-02-28", "operation" : "PutItem", "key" : { "toUser" : $util.dynamodb.toDynamoDBJson($ctx.args.toUser), "id" : $util.dynamodb.toDynamoDBJson($util.autoId()) }, "attributeValues" : { "fromUser" : $util.dynamodb.toDynamoDBJson($context.identity.username), "content" : $util.dynamodb.toDynamoDBJson($ctx.args.content), } }

이 예제에서는 $context.identity.username을 사용합니다. 그러면 Amazon Cognito 사용자 AWS Identity and Access Management 또는 사용자의 사용자 정보가 반환됩니다. 응답 템플릿은 $util.toJson($ctx.result)의 단순 전달입니다. 저장하고 스키마 페이지로 돌아갑니다. 그런 다음 권한 테이블을 데이터 소스로 사용하고 다음 요청 매핑 템플릿을 사용하여 newMessage() 구독용 해석기를 연결합니다.

{ "version": "2018-05-29", "operation": "GetItem", "key": { "username": $util.dynamodb.toDynamoDBJson($ctx.identity.username), }, }

그러고 나면 다음과 같은 응답 매핑 템플릿과 Permissions 테이블의 데이터를 사용하여 권한 부여 확인을 수행합니다.

#if(! ${context.result}) $utils.unauthorized() #elseif(${context.identity.username} != ${context.arguments.toUser}) $utils.unauthorized() #elseif(! ${context.result.isAuthorizedForSubscriptions}) $utils.unauthorized() #else ##User is authorized, but we return null to continue null #end

여기서는 세 가지 권한 부여 확인을 수행하려고 합니다. 첫 번째는 결과가 반환되는지 확인합니다. 두 번째는 사용자가 다른 사람에게 해당되는 메시지를 구독하고 있지 않은지 확인합니다. 세 번째는 BOOL로 저장된 isAuthorizedForSubscriptions의 DynamoDB 속성을 확인하여 사용자가 모든 필드를 구독하도록 허용되었는지 확인합니다.

테스트를 위해 Amazon Cognito 사용자 풀과 “Nadia”라는 사용자를 사용하여 AWS AppSync 콘솔에 로그인한 후 다음 GraphQL 구독을 실행할 수 있습니다.

subscription AuthorizedSubscription { newMessage(toUser: "Nadia") { id toUser fromUser content } }

Permissions 테이블에서 isAuthorizedForSubscriptionstrue로 설정된 Nadia의 키 속성 username에 대한 레코드가 있는 경우 성공적인 응답이 표시됩니다. 위의 username 쿼리에서 다른 newMessage()을 시도하면 오류가 반환됩니다.