Asset Library: Authentication token support

This introduces access tokens to Asset Libraries.

* The access tokens use the same Bearer Authentication as used by
extensions.

* This reset the token if "use_auth_token" is disabled. Actually it set
it to random values and then set it to null, to avoid it lingering in
memory. This could also be implemented for extensions repository.

Pull Request: https://projects.blender.org/blender/blender/pulls/162163
This commit is contained in:
Dalai Felinto 2026-08-06 12:39:09 +02:00
parent 963c5719af
commit a9e061dc90
19 changed files with 203 additions and 20 deletions

View file

@ -428,6 +428,7 @@ _downloaders: list[_RemoteAssetListingDownloader] = []
# Called directly from C++ code.
def remote_asset_library_sync(
asset_library_url: str,
asset_library_auth_token: str,
asset_library_local_path: Path,
only_if_older_than_sec=0,
) -> None:
@ -473,6 +474,7 @@ def remote_asset_library_sync(
# Create the downloader and start downloading.
downloader = listing_downloader.RemoteAssetListingDownloader(
asset_library_url,
asset_library_auth_token,
asset_library_local_path,
on_update_callback=_remote_asset_library_sync_update,
on_done_callback=_remote_asset_library_sync_done,
@ -555,14 +557,14 @@ def _remote_asset_library_sync_all_periodic():
continue
if not asset_lib.use_remote_url:
continue
remote_asset_library_sync(asset_lib.remote_url, Path(asset_lib.path),
remote_asset_library_sync(asset_lib.remote_url, asset_lib.auth_token, Path(asset_lib.path),
only_if_older_than_sec=REMOTE_ASSET_LIBS_AUTOSYNC_PERIOD_SEC)
# The online essentials library is not listed in the 'asset_libraries' list above, because it's not a preference.
if prefs.asset_libraries.use_online_essentials:
remote_url = bpy.types.AssetLibrary.online_assets_url()
cache_path = bpy.types.AssetLibrary.online_assets_cache_path()
remote_asset_library_sync(remote_url, Path(cache_path),
remote_asset_library_sync(remote_url, "", Path(cache_path),
only_if_older_than_sec=REMOTE_ASSET_LIBS_AUTOSYNC_PERIOD_SEC)

View file

@ -35,6 +35,7 @@ _preview_downloaders: dict[str, AssetDownloader] = {}
def download_asset_file(
asset_library_url: str,
asset_library_auth_token: str,
asset_library_local_path: Path,
asset_url: str,
asset_hash: str,
@ -45,6 +46,8 @@ def download_asset_file(
identifier of this library (to create a downloader per library), as well
as for resolving relative URLs.
:param asset_library_auth_token: Optional authentication token for bearer authentication.
:param asset_library_local_path: Root path of the local asset cache. Used to
resolve relative `save_to` paths, but also to find the HTTP metadata
cache for this asset library (for conditional downloads).
@ -68,6 +71,7 @@ def download_asset_file(
except KeyError:
downloader = AssetDownloader(
asset_library_url,
asset_library_auth_token,
asset_library_local_path,
reporter=AssetReporter(asset_library_url=asset_library_url),
on_queue_empty_callback=on_asset_download_queue_empty,
@ -93,6 +97,7 @@ def download_asset_file(
def download_preview(
asset_library_url: str,
asset_library_auth_token: str,
asset_library_local_path: Path,
preview_url: str,
preview_hash: str,
@ -103,6 +108,8 @@ def download_preview(
identifier of this library (to create a downloader per library), as well
as for resolving relative URLs.
:param asset_library_auth_token: Authentication tokens optionally required by some servers.
:param asset_library_local_path: Root path of the local asset cache. Used to
resolve relative `save_to` paths, but also to find the HTTP metadata
cache for this asset library (for conditional downloads).
@ -137,6 +144,7 @@ def download_preview(
except KeyError:
downloader = AssetDownloader(
asset_library_url,
asset_library_auth_token,
asset_library_local_path,
reporter=PreviewReporter(),
on_queue_empty_callback=None,
@ -261,6 +269,7 @@ class AssetDownloader:
def __init__(
self,
remote_url: str,
auth_token: str,
local_path: Path | str,
*,
reporter: http_dl.DownloadReporter,
@ -272,6 +281,8 @@ class AssetDownloader:
:param remote_url: Base URL of the remote asset library server.
:param auth_token: Optional (may be empty) authentication token.
:param local_path: The directory to download the index files to.
:param on_download_done_callback: called with one parameter (this
@ -289,14 +300,16 @@ class AssetDownloader:
# Work around a limitation of Blender, see bug report #139720 for details.
self.on_timer_event = self.on_timer_event # type: ignore[method-assign]
http_headers = {'X-Blender': "{:d}.{:d}".format(*bpy.app.version)}
if auth_token:
http_headers['Authorization'] = "Bearer {:s}".format(auth_token)
self._downloader_options = http_dl.DownloaderOptions(
metadata_provider=http_dl.MetadataProviderFilesystem(
cache_location=self._locator.http_metadata_cache_location,
),
timeout=300,
http_headers={
'X-Blender': "{:d}.{:d}".format(*bpy.app.version),
},
http_headers=http_headers,
num_parallel_downloads=num_parallel_downloads,
)

View file

@ -238,6 +238,7 @@ class RemoteAssetListingDownloader:
def __init__(
self,
remote_url: str,
auth_token: str,
local_path: Path | str,
on_update_callback: OnUpdateCallback,
on_done_callback: OnDoneCallback,
@ -250,6 +251,8 @@ class RemoteAssetListingDownloader:
blender_asset_library_openapi.yaml for the files downloaded from
there.
:param asset_library_auth_token: Optional authentication token for bearer authentication.
:param local_path: The directory to download the index files to.
:param on_update_callback: Called with one parameter (this
@ -301,16 +304,20 @@ class RemoteAssetListingDownloader:
cache_location=self._locator.http_metadata_cache_location,
))
http_headers = {
'Accept': 'application/json',
'X-Blender': "{:d}.{:d}".format(*bpy.app.version),
}
if auth_token:
http_headers['Authorization'] = "Bearer {:s}".format(auth_token)
# Create the background downloader object now, so that it
# (hypothetically in some future) can be adjusted before the actual
# downloading begins.
self._bg_downloader = http_dl.BackgroundDownloader(
options=http_dl.DownloaderOptions(
metadata_provider=self._http_metadata_provider,
http_headers={
'Accept': 'application/json',
'X-Blender': "{:d}.{:d}".format(*bpy.app.version),
},
http_headers=http_headers,
timeout=300,
max_disk_size_bytes=MAX_JSON_FILE_SIZE_MB * 1024 * 1024,
),

View file

@ -176,6 +176,8 @@ class AssetLibrary {
*/
virtual std::optional<StringRefNull> remote_url() const;
virtual std::optional<StringRefNull> auth_token() const;
AssetCatalogService &catalog_service() const;
/**

View file

@ -29,10 +29,13 @@ namespace asset_system {
struct RemoteLibraryDefinitionRef {
StringRefNull remote_url;
StringRefNull cache_dirpath;
std::optional<StringRefNull> auth_token;
RemoteLibraryDefinitionRef(const bUserAssetLibrary &library_definition);
RemoteLibraryDefinitionRef(StringRefNull remote_url, StringRefNull cache_dirpath)
: remote_url(remote_url), cache_dirpath(cache_dirpath)
RemoteLibraryDefinitionRef(StringRefNull remote_url,
StringRefNull cache_dirpath,
std::optional<StringRefNull> auth_token = std::nullopt)
: remote_url(remote_url), cache_dirpath(cache_dirpath), auth_token(auth_token)
{
}
};

View file

@ -259,6 +259,11 @@ std::optional<StringRefNull> AssetLibrary::remote_url() const
return {};
};
std::optional<StringRefNull> AssetLibrary::auth_token() const
{
return std::nullopt;
}
AssetCatalogService &AssetLibrary::catalog_service() const
{
std::lock_guard lock{catalog_service_mutex_};

View file

@ -36,7 +36,8 @@ void AllAssetLibrary::force_remote_listing_download() const
[&](AssetLibrary &nested) {
const std::optional<StringRefNull> url = nested.remote_url();
if (url.has_value()) {
remote_library_request_download(RemoteLibraryDefinitionRef{*url, nested.root_path()});
remote_library_request_download(
RemoteLibraryDefinitionRef{*url, nested.root_path(), nested.auth_token()});
}
},
/*include_all_library=*/false);

View file

@ -55,7 +55,11 @@ static CLG_LogRef LOG = {"asset.remote_library"};
namespace blender::asset_system {
RemoteLibraryDefinitionRef::RemoteLibraryDefinitionRef(const bUserAssetLibrary &library_definition)
: remote_url(library_definition.remote_url), cache_dirpath(library_definition.dirpath)
: remote_url(library_definition.remote_url),
cache_dirpath(library_definition.dirpath),
auth_token(library_definition.auth_token ?
std::optional<StringRefNull>(library_definition.auth_token) :
std::nullopt)
{
BLI_assert((library_definition.flag & ASSET_LIBRARY_USE_REMOTE_URL) != 0);
}
@ -78,7 +82,8 @@ RemoteAssetLibrary::RemoteAssetLibrary(const eAssetLibraryType library_type,
void RemoteAssetLibrary::force_remote_listing_download() const
{
remote_library_request_download(RemoteLibraryDefinitionRef{remote_url_, root_path()});
remote_library_request_download(
RemoteLibraryDefinitionRef{remote_url_, root_path(), auth_token()});
}
std::optional<eAssetImportMethod> RemoteAssetLibrary::import_method() const
@ -148,6 +153,22 @@ bool PreferencesRemoteAssetLibrary::is_enabled() const
return (library_definition->flag & ASSET_LIBRARY_DISABLED) == 0;
}
std::optional<StringRefNull> PreferencesRemoteAssetLibrary::auth_token() const
{
const bUserAssetLibrary *library_definition = user_library_.user_asset_library();
if (library_definition == nullptr) {
return std::nullopt;
}
StringRefNull token(library_definition->auth_token);
if (token.is_empty()) {
return std::nullopt;
}
BLI_assert(library_definition->flag & ASSET_LIBRARY_USE_AUTH_TOKEN);
return token;
}
/** \} */
/* -------------------------------------------------------------------- */
@ -628,12 +649,14 @@ void remote_library_request_download(const RemoteLibraryDefinitionRef &library_d
"from pathlib import Path\n"
"\n"
"bl_pkg.remote_asset_library_sync(\n"
" library_url, Path(library_path),\n"
" library_url, auth_token, Path(library_path)\n"
")\n";
std::unique_ptr locals = bke::idprop::create_group("locals");
IDP_AddToGroup(locals.get(), IDP_NewString(library_definition.remote_url, "library_url"));
IDP_AddToGroup(locals.get(), IDP_NewString(library_definition.cache_dirpath, "library_path"));
IDP_AddToGroup(locals.get(),
IDP_NewString(library_definition.auth_token.value_or(""), "auth_token"));
/* TODO: report errors in the UI somehow. */
BPY_run_string_exec_with_locals(nullptr, script, *locals);
@ -723,12 +746,13 @@ static std::optional<std::string> remote_library_request_asset_download_file(
"from pathlib import Path\n"
"\n"
"_result = asset_dl.download_asset_file(\n"
" library_url, Path(library_path),\n"
" library_url, auth_token, Path(library_path),\n"
" asset_url, asset_hash, Path(dst_filepath),\n"
")\n";
std::unique_ptr locals = bke::idprop::create_group("locals");
IDP_AddToGroup(locals.get(), IDP_NewString(*library.remote_url(), "library_url"));
IDP_AddToGroup(locals.get(), IDP_NewString(library.auth_token().value_or(""), "auth_token"));
IDP_AddToGroup(locals.get(), IDP_NewString(library.root_path(), "library_path"));
IDP_AddToGroup(locals.get(), IDP_NewString(dst_filepath, "dst_filepath"));
IDP_AddToGroup(locals.get(), IDP_NewString(asset_url.url, "asset_url"));
@ -868,12 +892,14 @@ void remote_library_request_preview_download(const bContext &C,
"from pathlib import Path\n"
"\n"
"asset_dl.download_preview(\n"
" library_url, Path(library_path),\n"
" library_url, library_auth_token, Path(library_path),\n"
" preview_url, preview_hash, Path(dst_filepath),\n"
")\n";
std::unique_ptr locals = bke::idprop::create_group("locals");
IDP_AddToGroup(locals.get(), IDP_NewString(*library_url, "library_url"));
IDP_AddToGroup(locals.get(),
IDP_NewString(library.auth_token().value_or(""), "library_auth_token"));
IDP_AddToGroup(locals.get(), IDP_NewString(library.root_path(), "library_path"));
IDP_AddToGroup(locals.get(), IDP_NewString(*preview_url, "preview_url"));
IDP_AddToGroup(locals.get(), IDP_NewString(*preview_hash, "preview_hash"));

View file

@ -41,6 +41,7 @@ class PreferencesRemoteAssetLibrary : public RemoteAssetLibrary {
public:
PreferencesRemoteAssetLibrary(const bUserAssetLibrary &custom_library);
std::optional<AssetLibraryReference> library_reference() const override;
std::optional<StringRefNull> auth_token() const override;
bool is_enabled() const;
};

View file

@ -122,6 +122,12 @@ bool BKE_preferences_asset_library_is_valid(const UserDef *userdef,
void BKE_preferences_asset_library_default_add(struct UserDef *userdef) ATTR_NONNULL();
void BKE_preferences_asset_library_read_data(struct BlendDataReader *reader,
struct bUserAssetLibrary *library);
void BKE_preferences_asset_library_write_data(struct BlendWriter *writer,
const struct bUserAssetLibrary *library);
/** \} */
/* -------------------------------------------------------------------- */

View file

@ -38,6 +38,7 @@
#include "BKE_idprop.hh"
#include "BKE_main.hh"
#include "BKE_node.hh"
#include "BKE_preferences.h"
#include "BKE_screen.hh"
#include "BKE_studiolight.h"
@ -377,7 +378,10 @@ void BKE_blender_userdef_data_free(UserDef *userdef, bool clear_fonts)
userdef->autoexec_paths.free_no_destruct();
userdef->script_directories.free_no_destruct();
userdef->asset_libraries.free_no_destruct();
for (bUserAssetLibrary &library_ref : userdef->asset_libraries.items_mutable()) {
BKE_preferences_asset_library_remove(userdef, &library_ref);
}
for (bUserExtensionRepo &repo_ref : userdef->extension_repos.items_mutable()) {
MEM_SAFE_DELETE(repo_ref.access_token);

View file

@ -88,6 +88,7 @@ bUserAssetLibrary *BKE_preferences_asset_library_add(UserDef *userdef,
void BKE_preferences_asset_library_remove(UserDef *userdef, bUserAssetLibrary *library)
{
MEM_delete(library->auth_token);
BLI_freelinkN(&userdef->asset_libraries, library);
}
@ -190,6 +191,21 @@ void BKE_preferences_asset_library_default_add(UserDef *userdef)
library->dirpath, sizeof(library->dirpath), documents_path, N_("Blender"), N_("Assets"));
}
void BKE_preferences_asset_library_read_data(BlendDataReader *reader, bUserAssetLibrary *library)
{
if (library->auth_token) {
BLO_read_string(reader, &library->auth_token);
}
}
void BKE_preferences_asset_library_write_data(BlendWriter *writer,
const bUserAssetLibrary *library)
{
if (library->auth_token) {
writer->write_string(library->auth_token);
}
}
bUserAssetLibrary *BKE_preferences_remote_asset_library_add(UserDef *userdef,
const char *name,
const char *remote_url)

View file

@ -4139,6 +4139,10 @@ static BHead *read_userdef(BlendFileData *bfd, FileData *fd, BHead *bhead)
IDP_BlendDataRead(reader, &addon.prop);
}
for (bUserAssetLibrary &asset_library_ref : user->asset_libraries) {
BKE_preferences_asset_library_read_data(reader, &asset_library_ref);
}
for (bUserExtensionRepo &repo_ref : user->extension_repos) {
BKE_preferences_extension_repo_read_data(reader, &repo_ref);
}

View file

@ -1315,6 +1315,7 @@ static void write_userdef(BlendWriter *writer, const UserDef *userdef)
for (const bUserAssetLibrary &asset_library_ref : userdef->asset_libraries) {
writer->write_struct(&asset_library_ref);
BKE_preferences_asset_library_write_data(writer, &asset_library_ref);
}
for (const bUserExtensionRepo &repo_ref : userdef->extension_repos) {

View file

@ -184,7 +184,8 @@ static void draw_library_list(const bContext &C, ui::Layout &layout)
ui::TreeViewBuilder::build_tree_view(C, *tree_view, layout);
}
static void draw_active_library_settings(ui::Layout &layout,
static void draw_active_library_settings(const bContext *C,
ui::Layout &layout,
const AnyAssetLibraryDefinition &library)
{
if (library.type == ASSET_LIBRARY_ESSENTIALS) {
@ -217,6 +218,23 @@ static void draw_active_library_settings(ui::Layout &layout,
IFACE_("Repository URL"));
}
layout.prop(&library_ptr, "import_method", UI_ITEM_NONE, IFACE_("Import Method"), ICON_NONE);
if (ui::Layout *panel = layout.panel(C, "advanced", true, IFACE_("Advanced"))) {
panel->use_property_split_set(true);
ui::Layout &column = panel->column(true, IFACE_("Authentication"));
column.prop(&library_ptr, "use_auth_token", UI_ITEM_NONE, std::nullopt, ICON_NONE);
if (library.user_library->flag & ASSET_LIBRARY_USE_AUTH_TOKEN) {
column.prop(&library_ptr,
RNA_struct_find_property(&library_ptr, "auth_token"),
RNA_NO_INDEX,
0,
UI_ITEM_NONE,
IFACE_("Secret"),
library.user_library->auth_token ? ICON_LOCKED : ICON_UNLOCKED,
std::nullopt);
}
}
}
else {
layout.prop(&library_ptr, "path", UI_ITEM_NONE, std::nullopt, ICON_NONE);
@ -260,7 +278,7 @@ void userpref_asset_libraries_panel_draw(const bContext *C, Panel *panel)
layout.separator();
draw_active_library_settings(layout, libraries[U.active_asset_library]);
draw_active_library_settings(C, layout, libraries[U.active_asset_library]);
}
} // namespace blender

View file

@ -58,6 +58,7 @@ enum eAssetLibrary_Flag : int {
ASSET_LIBRARY_RELATIVE_PATH = (1 << 0),
ASSET_LIBRARY_DISABLED = (1 << 1),
ASSET_LIBRARY_USE_REMOTE_URL = (1 << 2),
ASSET_LIBRARY_USE_AUTH_TOKEN = (1 << 3),
};
enum class AssetAccess : int8_t {

View file

@ -659,6 +659,11 @@ struct bUserAssetLibrary {
/** Only for remote asset libraries (#ASSET_LIBRARY_USE_REMOTE_URL is set). Update using
* #BKE_preferences_remote_asset_library_url_set() only. */
char remote_url[/*FILE_MAX*/ 1024];
/**
* Secret access token for remote repositories (allocated).
* Only use when #ASSET_LIBRARY_USE_AUTH_TOKEN is set.
*/
char *auth_token = nullptr;
short import_method = ASSET_IMPORT_PACK; /* eAssetImportMethod */
short flag = ASSET_LIBRARY_RELATIVE_PATH; /* eAssetLibrary_Flag */

View file

@ -239,6 +239,7 @@ static const EnumPropertyItem rna_enum_preferences_extension_repo_source_type_it
# include "BLI_listbase.hh"
# include "BLI_math_vector_c.hh"
# include "BLI_memory_cache.hh"
# include "BLI_rand_c.hh"
# include "BLI_string.hh"
# include "BLI_string_utf8.hh"
# include "BLI_string_utils.hh"
@ -488,6 +489,54 @@ static void rna_userdef_asset_libraries_use_online_essentials_update(bContext *C
rna_userdef_update(CTX_data_main(C), CTX_data_scene(C), ptr);
}
static void rna_userdef_asset_library_auth_token_get(PointerRNA *ptr, char *value)
{
bUserAssetLibrary *library = static_cast<bUserAssetLibrary *>(ptr->data);
if (library->auth_token) {
strcpy(value, library->auth_token);
}
else {
value[0] = '\0';
}
}
static int rna_userdef_asset_library_auth_token_length(PointerRNA *ptr)
{
bUserAssetLibrary *library = static_cast<bUserAssetLibrary *>(ptr->data);
return (library->auth_token) ? strlen(library->auth_token) : 0;
}
static void rna_userdef_asset_library_auth_token_set(PointerRNA *ptr, const char *value)
{
bUserAssetLibrary *library = static_cast<bUserAssetLibrary *>(ptr->data);
if (library->auth_token) {
/* Replace the existing token with random characters to avoid leaving it in memory. */
RNG *rng = BLI_rng_new_srandom(uint(intptr_t(library->auth_token)));
BLI_rng_get_char_n(rng, library->auth_token, strlen(library->auth_token));
BLI_rng_free(rng);
/* Now we can more comfortably delete the token. */
MEM_delete(library->auth_token);
library->auth_token = nullptr;
}
if (value[0]) {
const StringRef auth_token = StringRef{value}.trim();
library->auth_token = BLI_strdupn(auth_token.data(), auth_token.size());
}
}
static void rna_userdef_asset_library_use_auth_token_update(bContext * /*C*/, PointerRNA *ptr)
{
bUserAssetLibrary *library = static_cast<bUserAssetLibrary *>(ptr->data);
const bool use_auth_token = (library->flag & ASSET_LIBRARY_USE_AUTH_TOKEN) != 0;
if (!use_auth_token && library->auth_token) {
/* Make sure the token is wiped if we are not using it. */
rna_userdef_asset_library_auth_token_set(ptr, "");
}
}
/**
* Use sparingly as a sync may be time consuming.
* Any change that may cause loading remote data to change behavior
@ -7006,6 +7055,23 @@ static void rna_def_userdef_filepaths_asset_library(BlenderRNA *brna)
RNA_def_property_boolean_sdna(prop, nullptr, "flag", ASSET_LIBRARY_USE_REMOTE_URL);
RNA_def_property_ui_text(prop, "Use Remote", "Synchronize the asset library with a remote URL");
RNA_def_property_clear_flag(prop, PROP_EDITABLE);
prop = RNA_def_property(srna, "use_auth_token", PROP_BOOLEAN, PROP_NONE);
RNA_def_property_boolean_sdna(prop, nullptr, "flag", ASSET_LIBRARY_USE_AUTH_TOKEN);
RNA_def_property_ui_text(
prop, "Requires Access Token", "Asset library requires an authentication token");
RNA_def_property_flag(prop, PROP_CONTEXT_UPDATE);
RNA_def_property_update(prop, 0, "rna_userdef_asset_library_use_auth_token_update");
prop = RNA_def_property(srna, "auth_token", PROP_STRING, PROP_PASSWORD);
RNA_def_property_ui_text(
prop,
"Access Token",
"Personal authentication token, may be required by some asset libraries");
RNA_def_property_string_funcs(prop,
"rna_userdef_asset_library_auth_token_get",
"rna_userdef_asset_library_auth_token_length",
"rna_userdef_asset_library_auth_token_set");
}
static void rna_def_userdef_filepaths_extension_repo(BlenderRNA *brna)

View file

@ -96,6 +96,7 @@ class ListingDownloaderTest(unittest.TestCase):
Downloader = listing_downloader.RemoteAssetListingDownloader
dl = Downloader(
remote_url="http://localhost/",
auth_token="",
local_path="/tmp/does-not-matter-we-do-not-write",
on_update_callback=lambda downloader: None,
on_done_callback=lambda downloader: None,
@ -151,6 +152,7 @@ class ListingDownloaderTest(unittest.TestCase):
Downloader = listing_downloader.RemoteAssetListingDownloader
dl = Downloader(
remote_url="http://localhost/",
auth_token="",
local_path="/tmp/does-not-matter-we-do-not-write",
on_update_callback=lambda downloader: None,
on_done_callback=lambda downloader: None,