Keyboard shortcuts

Press ← or → to navigate between chapters

Press ? to show this help

Press Esc to hide this help

함수의 외부 API 호출과 OAuth

함수는 기본으로 외부 API를 호출할 수 없습니다. Data Connection에 외부 시스템의 소스를 만들고 내보내기와 코드 가져오기를 허용한 뒤 함수 저장소로 가져와야 합니다. TSv2와 Python은 소스 별칭이 권장되며 TSv1은 생성된 소스 심벌을 사용합니다.

소스와 전송 설정

소스 생성 후 Connection settings에서 내보내기와 Code Repositories 가져오기를 구성합니다. TSv1에서 코드로 참조할 이름은 Code import configuration의 소스 API 이름입니다.

인증서 체인을 완전하게 구성해야 합니다. 웹훅에서 성공하더라도 함수 런타임이 같지는 않으므로 UNABLE_TO_GET_ISSUER_CERT가 발생할 수 있습니다. 연결 문제 해결의 소스 터미널 인증서 확인을 사용하며 검증을 끄는 방법으로 대체하지 않습니다.

소스의 제공 클라이언트에는 서버 및 클라이언트 인증서와 함수 환경의 프록시 등 외부 통신 설정이 포함됩니다. 일반 외부 URL이나 Foundry 내부 URL에 직접 요청하는 코드는 모든 실행 환경에서 동작한다고 보장할 수 없습니다.

HTTPS 소스의 읽기 함수

아래 예제는 텍스트를 반환하는 HTTPS 소스를 가져온 경우입니다. 호출할 소스를 코드에도 선언해야 합니다.

TypeScript v1

import { ExternalSystems, Function } from "@foundry/functions-api";
import { MySource } from "@foundry/external-systems/sources";

export class MyExternalFunctions {
    @ExternalSystems({ sources: [MySource] })
    @Function()
    public async myExternalFunction(): Promise<string> {
        const { url } = MySource.getHttpsConnection();
        const response = await MySource.fetch(url);
        if (!response.ok) throw new Error(`External API returned HTTP ${response.status}`);
        return response.text();
    }
}

TypeScript v2

myExternalFunction.ts에 작성하고 mySourceAlias를 실제로 구성합니다.

import { getSource, getHttpsConnection, getFetch } from "@palantir/functions-sources";

export const config = { sources: ["mySourceAlias"] };

export default async function myExternalFunction(): Promise<string> {
    const source = await getSource("mySourceAlias");
    const { url } = getHttpsConnection(source);
    const fetch = await getFetch(source);
    const response = await fetch(url);
    if (!response.ok) throw new Error(`External API returned HTTP ${response.status}`);
    return response.text();
}

Python

from functions.api import function
from functions.sources import get_source


@function(sources=["mySourceAlias"])
def my_external_function() -> str:
    source = get_source("mySourceAlias")
    connection = source.get_https_connection()
    response = connection.get_client().get(connection.url)
    response.raise_for_status()
    return response.text

Live Preview에서도 외부 시스템으로 실제 요청을 보낼 수 있습니다.

속성과 비밀값 접근

내용TSv1TSv2Python
기본 URLMySource.getHttpsConnection().urlgetHttpsConnection(source).urlsource.get_https_connection().url
MySecret 비밀값MySource.getSecret("MySecret")source.secrets["MySecret"]source.get_secret("MySecret")
구성된 HTTP 전송MySource.fetchawait getFetch(source)source.get_https_connection().get_client()

MySecret은 소스에 실제로 저장한 비밀 항목의 예제 키입니다. 비밀값을 로그나 응답에 노출하지 않습니다. HTTPS 클라이언트를 제공하지 않는 소스에서는 비밀값 외의 일반 속성에 접근할 수 없습니다. PostgreSQL 소스의 hostname 같은 속성이 예입니다.

타사 클라이언트와 HTTP agent

서버리스와 Live Preview에서 기본 fetch나 HTTP agent를 그대로 쓰는 타사 클라이언트는 지원하지 않습니다. 소스가 제공한 전송 함수나 agent로 재정의해야 합니다. 이를 적용할 수 없는 클라이언트는 전용 배포의 지원 범위를 검토해야 합니다.

TSv2에서 agent를 지원하는 Axios에 연결하는 예제입니다. axios를 프로젝트 라이브러리에 추가해야 합니다.

import axios from "axios";
import { getSource, getHttpAgent, getHttpsConnection } from "@palantir/functions-sources";

export const config = { sources: ["mySourceAlias"] };

export default async function readWithAxios(): Promise<string> {
    const source = await getSource("mySourceAlias");
    const agent = await getHttpAgent(source);
    const { url } = getHttpsConnection(source);
    const response = await axios.get<string>(url, {
        httpsAgent: agent,
        responseType: "text",
    });
    return response.data;
}

파일 이름은 readWithAxios.ts입니다. Axios의 기본 상태 검사는 성공이 아닌 응답을 오류로 처리합니다.

OAuth outbound application

OAuth 2.0 API에는 Control Panel의 outbound application을 구성하고 REST API 소스의 인증 방식으로 연결합니다. TSv2와 Python은 호출 사용자의 OAuth 접근 토큰을 세션 자격 증명으로 받아 그 사용자를 대신하여 요청합니다.

  • TSv1은 소스의 OAuth 토큰을 직접 가져오지 못합니다. outbound application으로 인증한 REST API 소스의 웹훅을 통해 호출할 수 있습니다.
  • Deployed 모드에서는 OAuth 토큰 갱신을 지원하지 않습니다. 실행 중 사용자 토큰이 만료되면 자동 갱신할 수 없으므로 outbound application에는 서버리스 경로가 권장됩니다.
  • Workshop의 함수 기반 변수나 콘텐츠 함수는 대화형 OAuth 승인 창을 열 수 없습니다. 아직 승인하지 않은 사용자의 호출은 실패합니다. 함수 기반 액션으로 감싸거나 같은 outbound application에 대해 다른 대화형 화면에서 먼저 승인을 완료해야 합니다.

제공 클라이언트의 자동 헤더

OAuth 소스를 myOAuthSourceAlias로 구성하고 원천이 GET /api/v1/resource를 제공한다고 가정합니다. 제공 클라이언트는 Authorization 헤더를 자동 삽입합니다.

from functions.api import function
from functions.sources import get_source


@function(sources=["myOAuthSourceAlias"])
def call_external_api() -> str:
    connection = get_source("myOAuthSourceAlias").get_https_connection()
    response = connection.get_client().get(connection.url + "/api/v1/resource", timeout=10)
    response.raise_for_status()
    return response.text
import { getSource, getHttpsConnection, getFetch } from "@palantir/functions-sources";

export const config = { sources: ["myOAuthSourceAlias"] };

export default async function callExternalApi(): Promise<string> {
    const source = await getSource("myOAuthSourceAlias");
    const { url } = getHttpsConnection(source);
    const fetch = await getFetch(source);
    const response = await fetch(url + "/api/v1/resource");
    if (!response.ok) throw new Error(`External API returned HTTP ${response.status}`);
    return response.text();
}

네이티브 클라이언트에 토큰 직접 삽입

다음 예제는 인증 부분을 직접 구성하는 방식입니다. 토큰을 넣는 것만으로 인증서와 프록시가 준비되지는 않습니다. 별도 클라이언트의 전송 설정이 소스 정책과 해당 실행 환경을 충족해야 하며, 일반적인 경우에는 앞의 제공 클라이언트를 사용합니다.

Python은 Refreshable 자격 증명에서 현재 토큰을 읽습니다.

import requests
from functions.api import function
from functions.sources import get_source
from external_systems.sources import OauthCredentials, Refreshable, SourceCredentials


@function(sources=["myOAuthSourceAlias"])
def call_external_api_manual() -> str:
    source = get_source("myOAuthSourceAlias")
    credentials: Refreshable[SourceCredentials] = source.get_session_credentials()
    current = credentials.get()
    if not isinstance(current, OauthCredentials):
        raise ValueError("Expected OAuth credentials")
    response = requests.get(
        source.get_https_connection().url + "/api/v1/resource",
        headers={"Authorization": f"Bearer {current.access_token}"},
        timeout=10,
    )
    response.raise_for_status()
    return response.text

TSv2는 sessionCredentials에서 oauth 타입을 확인합니다.

import { getSource, getHttpsConnection } from "@palantir/functions-sources";

export const config = { sources: ["myOAuthSourceAlias"] };

export default async function callExternalApiManual(): Promise<string> {
    const source = await getSource("myOAuthSourceAlias");
    const credentials = await source.sessionCredentials?.get();
    if (!credentials || credentials.type !== "oauth") {
        throw new Error("Expected OAuth credentials");
    }
    const { url } = getHttpsConnection(source);
    const response = await fetch(url + "/api/v1/resource", {
        headers: { Authorization: `Bearer ${credentials.accessToken}` },
    });
    if (!response.ok) throw new Error(`External API returned HTTP ${response.status}`);
    return response.text();
}

OAuth 결과로 온톨로지 편집

다음 예제의 GET /v1/me는 호출 사용자의 문자열 id와 display_name을 반환해야 합니다. UserProfile에는 이 ID를 기본 키로 하고 표시 이름을 저장하는 속성이 있다고 가정합니다. 새 프로필을 생성하는 흐름이며 기존 프로필의 갱신과는 구분합니다.

Python은 실제 응답으로 편집 목록을 만듭니다.

from functions.api import function, OntologyEdit
from functions.sources import get_source
from ontology_sdk import FoundryClient
from ontology_sdk.ontology.objects import UserProfile


@function(sources=["myOAuthSourceAlias"], edits=[UserProfile])
def link_user_profile() -> list[OntologyEdit]:
    connection = get_source("myOAuthSourceAlias").get_https_connection()
    response = connection.get_client().get(connection.url + "/v1/me", timeout=10)
    response.raise_for_status()
    profile = response.json()
    edits = FoundryClient().ontology.edits()
    edits.objects.UserProfile.create(profile["id"], display_name=profile["display_name"])
    return edits.get_edits()

TSv2는 Client와 createEditBatch를 사용합니다.

import { getSource, getHttpsConnection, getFetch } from "@palantir/functions-sources";
import { UserProfile } from "@ontology/sdk";
import { Client } from "@osdk/client";
import { createEditBatch, Edits } from "@osdk/functions";

type OntologyEdit = Edits.Object<UserProfile>;
export const config = { sources: ["myOAuthSourceAlias"], edits: [UserProfile] };

export default async function linkUserProfile(client: Client): Promise<OntologyEdit[]> {
    const source = await getSource("myOAuthSourceAlias");
    const { url } = getHttpsConnection(source);
    const fetch = await getFetch(source);
    const response = await fetch(url + "/v1/me");
    if (!response.ok) throw new Error(`Failed to fetch profile: ${response.status}`);
    const profile = await response.json();
    const batch = createEditBatch<OntologyEdit>(client);
    batch.create(UserProfile, {
        userProfileId: profile.id,
        displayName: profile.display_name,
    });
    return batch.getEdits();
}

반환한 편집을 적용하는 함수 기반 액션을 Workshop이나 AIP Studio에서 실행하면 호출 사용자의 토큰으로 외부 요청을 하고 그 사용자에게 온톨로지 편집을 귀속합니다.

오류 조사

HTTP 401, Credentials expired and no refresh handler provided, Resolved source credentials are not present on the Source는 OAuth 및 outbound application 구성을 확인해야 합니다. 함수 직접 호출의 승인 창 제한과 Deployed 갱신 제한도 함께 봅니다.

HTTP 407: Proxy Authentication Required는 목적지 호스트가 egress 정책에 포함되지 않은 경우 발생할 수 있습니다. URL 결합도 확인해야 합니다. getHttpsConnection의 URL에는 끝 슬래시가 없으므로 다음은 잘못된 호스트가 됩니다.

"https://example.com" + "api/v1" = "https://example.comapi/v1"

경로 앞에 슬래시를 넣어 url + "/api/v1"로 구성해야 합니다. 재시도나 인증 토큰 교체만으로 잘못된 호스트 문제가 해결되지는 않습니다.