매개변수화된 뷰 사용

이 튜토리얼에서는 Spanner에서 파라미터화된 뷰를 사용하여 애플리케이션이 안전하게 전달하는 파라미터를 기반으로 데이터에 대한 액세스를 제한하는 방법을 보여줍니다.

파라미터화된 뷰에 대한 자세한 내용은 파라미터화된 뷰 개요를 참조하세요.

목표

  • 샘플 데이터로 테이블을 만듭니다.
  • 보안 컨텍스트 파라미터를 기반으로 데이터를 필터링하는 파라미터화된 뷰를 만듭니다.
  • 애플리케이션이 데이터베이스에 연결하고 파라미터화된 뷰에 액세스하는 데 사용하는 세분화된 액세스 제어를 위한 데이터베이스 역할을 만듭니다.
  • 기본 테이블에 대한 액세스 권한을 부여하지 않고 파라미터화된 뷰를 쿼리할 수 있는 권한을 새 역할에 부여합니다.
  • 보안 컨텍스트에서 파라미터를 전달하여 애플리케이션에서 파라미터화된 뷰를 쿼리합니다.

시작하기 전에

이 튜토리얼을 완료하려면 Spanner 인스턴스와 데이터베이스가 필요합니다. 없는 경우 Google Cloud 콘솔을 사용하여 데이터베이스 만들기 및 쿼리를 참조하여 인스턴스와 Spanner 데이터베이스를 만듭니다.

샘플 데이터 준비

  1. Google Cloud 콘솔에서 Spanner 페이지로 이동합니다.

    Spanner로 이동

  2. 사용할 인스턴스의 이름을 클릭한 후 이 튜토리얼에 사용할 데이터베이스를 선택합니다.

  3. 데이터베이스 개요 페이지의 탐색 메뉴에서 Spanner 스튜디오를 클릭합니다.

  4. 다음 DDL 문을 입력하여 checked_items 테이블을 만듭니다.

    CREATE TABLE checked_items(
      bag_id INT64,
      item_timestamp TIMESTAMP,
      loc_code STRING(3),
      scan_type STRING(1),
      location STRING(MAX),
      customer_id INT64
    ) PRIMARY KEY(bag_id);
    
  5. 실행을 클릭합니다.

  6. Spanner가 테이블을 만든 후 다음 문을 입력하여 샘플 데이터를 테이블에 삽입합니다.

    INSERT INTO checked_items (bag_id, item_timestamp, loc_code, scan_type, location, customer_id) VALUES
    (101, '2023-10-26 10:00:00', 'ABC', 'I', 'Warehouse A', 123),
    (102, '2023-10-26 10:15:30', 'DEF', 'O', 'Loading Dock B', 456),
    (103, '2023-10-26 10:30:45', 'GHI', 'I', 'Conveyor Belt 1', 789),
    (104, '2023-10-26 11:00:00', 'JKL', 'O', 'Shipping Area C', 101),
    (105, '2023-10-26 11:45:15', 'MNO', 'I', 'Sorting Station D', 202),
    (106, '2023-10-26 12:00:00', 'PQR', 'O', 'Truck Bay E', 303);
    
  7. 실행을 클릭합니다.

파라미터화된 뷰 만들기

SECURE_CONTEXT()에서 제공하는 app_end_userid 값을 기준으로 checked_items에서 행을 필터링하는 SQL SECURITY DEFINER로 뷰를 만듭니다. 이 함수는 애플리케이션에서 안전하게 전달된 파라미터 값을 검색합니다.

  1. 쿼리 편집기에서 다음 문을 입력하여 secure_checked_items 뷰를 만듭니다.

    CREATE VIEW secure_checked_items
    SQL SECURITY DEFINER AS
    (SELECT bag_id, item_timestamp, location
    FROM checked_items t
    WHERE customer_id = CAST(SECURE_CONTEXT('app_end_userid'), INT64));
    
  2. 실행을 클릭합니다.

액세스 권한 설정하기

사용자가 매개변수화된 뷰를 통해서만 데이터에 액세스할 수 있도록 하려면 세분화된 액세스 제어 역할을 만들고 뷰에 대한 SELECT 권한을 해당 역할에 부여합니다. 역할과 public 시스템 역할 모두 기본 테이블에 대한 권한이 없는지 확인합니다.

  1. 쿼리 편집기에서 다음 문을 입력하여 pv_user 역할을 만듭니다.

    CREATE ROLE pv_user;
    
  2. 뷰에 대한 액세스 권한을 부여합니다.

    GRANT SELECT ON VIEW secure_checked_items TO ROLE pv_user;
    
  3. 실행을 클릭합니다.

하지만 쿼리를 디버깅하기 위해 SECURE_CONTEXT()를 직접 호출해야 하는 경우 자신에게 spanner_secure_context_reader 역할을 부여합니다. 신뢰할 수 없는 에이전트에게 이 역할을 부여해서는 안 됩니다.

파라미터화된 뷰를 사용하여 데이터 쿼리

애플리케이션은 먼저 쿼리 요청의 보안 컨텍스트에서 파라미터를 설정한 후 뷰에 대해 표준 SELECT 쿼리를 실행하여 파라미터화된 뷰를 쿼리합니다. 뷰는 SECURE_CONTEXT() 함수를 사용하여 파라미터 값을 기준으로 결과를 필터링합니다.

다음 코드 샘플은 app_end_userid 보안 매개변수를 303로 설정하고 secure_checked_items 뷰를 쿼리하는 방법을 보여줍니다.

C#

static async Task QueryWithUserId(SpannerConnection connection, string userId)
{
    string query = "SELECT * FROM myuserdata";
    using (var command = connection.CreateSelectCommand(query))
    {
        command.ClientContext = new Google.Cloud.Spanner.Data.ClientContext {
            ClientContextModifier = (v1Context) => {
                v1Context.SecureContext["user_id"] = Value.ForString(userId);
            }
        };
        using (var reader = await command.ExecuteReaderAsync())
        {
            int rowCount = 0;
            while (await reader.ReadAsync())
            {
                Console.WriteLine($"UserID: {reader["userid"]}, UserName: {reader["username"]}, SecretData: {reader["secretdata"]}");
                rowCount++;
            }
            if (rowCount != 1) throw new Exception($"Expected 1 row, got {rowCount}");
        }
    }
}

Go

func queryWithUserID(ctx context.Context, client *spanner.Client, userID string) error {
	stmt := spanner.Statement{SQL: "SELECT * FROM myuserdata"}
	iter := client.Single().QueryWithOptions(ctx, stmt, spanner.QueryOptions{
		ClientContext: &spannerpb.RequestOptions_ClientContext{
			SecureContext: map[string]*structpb.Value{"user_id": structpb.NewStringValue(userID)},
		},
	})

	rowCount := 0
	err := iter.Do(func(r *spanner.Row) error {
		var id int64
		var name, secret string
		if err := r.Columns(&id, &name, &secret); err != nil {
			return err
		}
		if fmt.Sprintf("%d", id) != userID {
			return fmt.Errorf("unexpected UserID %d for user %s", id, userID)
		}
		fmt.Printf("UserID: %d, UserName: %s, SecretData: %s\n", id, name, secret)
		rowCount++
		return nil
	})
	if err != nil {
		return err
	}
	if rowCount != 1 {
		return fmt.Errorf("expected 1 row, got %d", rowCount)
	}
	return nil
}

자바

static void queryWithUserId(DatabaseClient dbClient, String userId) {
  RequestOptions.ClientContext clientContext =
      RequestOptions.ClientContext.newBuilder()
          .putSecureContext("user_id", Value.newBuilder().setStringValue(userId).build())
          .build();

  try (ResultSet resultSet =
      dbClient
          .singleUse()
          .executeQuery(
              Statement.of("SELECT * FROM MyUserData"), Options.clientContext(clientContext))) {
    int rowCount = 0;
    while (resultSet.next()) {
      long id = resultSet.getLong("UserID");
      if (!String.valueOf(id).equals(userId)) {
        throw new RuntimeException("Unexpected UserID " + id + " for user " + userId);
      }
      System.out.printf(
          "UserID: %d, UserName: %s, SecretData: %s%n",
          id, resultSet.getString("UserName"), resultSet.getString("SecretData"));
      rowCount++;
    }
    if (rowCount != 1) {
      throw new RuntimeException("Expected 1 row, got " + rowCount);
    }
  }
}

Python

def query_with_user_id(database, user_id: str):
    """Queries a parameterized view using secure context parameters."""
    with database.snapshot() as snapshot:
        results = snapshot.execute_sql(
            "SELECT * FROM MyUserData",
            request_options={
                "client_context": {
                    "secure_context": {"user_id": user_id}
                }
            },
        )
        for row in results:
            print(f"UserID: {row[0]}, UserName: {row[1]}, SecretData: {row[2]}")

이 쿼리는 customer_id이 303인 checked_items의 행만 반환합니다.

+--------+---------------------------+-------------+
| bag_id | item_timestamp            | location    |
+--------+---------------------------+-------------+
|    106 | 2023-10-26 12:00:00+00:00 | Truck Bay E |
+--------+---------------------------+-------------+
1 row.

애플리케이션이 pv_user 역할이 부여된 주 구성원을 사용하여 Spanner에 연결하는 경우 checked_items 테이블을 직접 쿼리하려고 하면 권한 거부 오류가 발생합니다.

다음 단계