feat: remove profiles from the authentication system#274
feat: remove profiles from the authentication system#274Cedric / ViaDézo1er (viadezo1er) wants to merge 15 commits into
Conversation
502151d to
8304c9d
Compare
|
Latest downloadable build artifacts for this PR commit
Available artifact names
|
14a4ffc to
21d58d0
Compare
This comment was marked as outdated.
This comment was marked as outdated.
first draft BREAKING-CHANGE: --profile and --prefer-profile removed Breaks any script using them
…n to use the new auth storage format orgid,email
remove dead code
The field is needed by OAuth but it's unused both bt and the backend Therefore exposing it is useless Now bt_cli is hardcoded as its value
dbdc091 to
dedf063
Compare
b73f2b8 to
fbe8a07
Compare
- Preserves separate OAuth and API-key login identities.
- Keeps multiple API keys for the same org distinct using key hints.
- Implements the intended precedence:
1. Explicit --api-key
2. --prefer-api-key
3. OAuth
4. BRAINTRUST_API_KEY
5. Stored API key
- Validates explicit/environment API keys against the requested org.
- Does not fall back to OAuth when a selected key is invalid or belongs to another org.
- Rejects API-key authentication in cross-org mode.
- Pins the exact API-key login selected by bt switch or bt init.
- Adds bt auth login --global/--local.
- Adds filtering to bt auth logins --org/--prefer-api-key.
- Changes bare bt auth logout to select from all saved logins instead of deleting the currently active one.
- Adds bt auth logout --oauth and retains --api-key-hint.
Config handling — src/config/mod.rs
- Makes global/local merging org-safe:
- A project is never inherited from another org.
- project and project_id remain coupled.
- Local project-without-org does not inherit the global org.
- Stores cross-org as:
```json
{ "org": "" }
```
- Ignores the legacy profile field.
- Preserves unknown config keys during updates.
- Only treats .bt/config.json as a local config; a bare .bt directory is not one.
- Adds separate filesystem discovery rules for bt init.
bt switch — src/switch.rs
- Starts with a saved-login picker:
- OAuth entries collapse into org choices.
- API keys remain separate and display their hints.
- Cross-org OAuth is selectable.
- Automatically selects a sole login/project.
- Prompts for project when multiple projects exist.
- Pins a selected API-key slot exactly.
- Implements global/local scope rules:
- --global does not search for local config.
- --local requires an existing .bt/config.json.
- Interactive ambiguity opens a scope picker.
- Non-interactive ambiguity errors before network work.
- Preserves unknown config fields.
- Fixes the scope-picker ANSI corruption you reported by passing plain labels to Dialoguer.
bt init — src/init.rs
- Adds:
- --here
- -f/--force
- Searches upward for .bt, .git, home, or filesystem root using the specified boundary rules.
- Resolves destination errors before authentication.
- Uses the same saved-login/project selection behavior as switch.
- Excludes cross-org from the picker because init requires a project.
- Writes org, project, and project_id.
- Reports permission/write failures with the affected path.
- Now exposes the real destination error instead of only saying:
```text
could not resolve `bt init` destination
```
Cross-org CLI/status — src/args.rs, src/main.rs, src/status.rs
- Normalizes these to canonical cross-org:
```sh
--org cross-org
--org ""
```
- Displays cross-org as cross-org in human and JSON status output.
- Prevents stale projects from being combined with cross-org context.
- Makes --prefer-api-key fail actionably from cross-org context.
fbe8a07 to
780a30b
Compare
before it was always the same name which could lead to corruption if multiple bt used it at the same time
`bt datasets snapshots create my-dataset` would use the name of the OAuth login when the credentials used were BRAINTRUST_API_KEY, ie not OAuth
Always show current auth in bt status even if it's from API key in env
27981a0 to
3677203
Compare
Andrew Kent (realark)
left a comment
There was a problem hiding this comment.
All hand-written, no LLM was involved in this PR description.
🐐
LGTM! Left a few things for your consideration but feel free to ignore at your discretion
| // Only prune once the collapsed store is durably on disk; otherwise | ||
| // the on-disk file still references the dropped duplicate and the | ||
| // next load must be able to retry the migration. | ||
| Ok(()) => prune_orphaned_migration_secrets(&store, &migrated), |
There was a problem hiding this comment.
I'm not sure how risky this is, but any concerns about migration dropping data in a way that isn't recoverable? Do we want to copy the old config to auth.json.bak or something like that before attempting migration?
There was a problem hiding this comment.
{
"profiles": {
"ced-test-1": {
"auth_kind": "oauth",
"custom field to test": "big brain",
"api_url": "https://api.braintrust.dev",
"app_url": "https://www.braintrust.dev",
"org_name": "ced-test-1",
"oauth_client_id": "bt_cli_default",
"oauth_access_expires_at": 1785022664,
"user_name": "Cédric Halber",
"email": "cedric@braintrustdata.com",
"api_key_hint": null
}
}
}The custom field is dropped.
{
"profiles": {
"ced-test-1": {
"auth_kind": "oauth",
"custom field to test": "big brain",
"api_url": "https://api.braintrust.dev",
"app_url": "https://www.braintrust.dev",
"org_name": "ced-test-1",
"oauth_client_id": "bt_cli_default",
"oauth_access_expires_at": 1785022664,
"user_name": "Cédric Halber",
"email": "cedric@braintrustdata.com",
"api_key_hint": null
}
},
{"does this fail":"a"}
}New bt fails to parse.
We might have users who fork the cli and change it... It has happened before.
So yes valid comment I'll backup the auth/config files.
| } | ||
|
|
||
| /// Human-facing spelling of the empty cross-org marker. | ||
| pub(crate) const CROSS_ORG_ALIAS: &str = "cross-org"; |
There was a problem hiding this comment.
Getting a little lost in the details, so I figured I'd just ask: will everything still work if users literally have an org in braintrust named cross-org? And/or do we need an explicit --cross-org flag to distinguish the two?
Probably not a super common thing, but it's generic enough that I wouldn't be shocked if we had a user doing this
There was a problem hiding this comment.
Ahhhh wonderful
So org names are global across data planes, and cross-org is a valid org name.
I created it to park the name to prevent its use in US/EU.
So the only person who can be affected by this for US/EU is me.
It could be an issues in self-hosted instances though.
I did think about a --cross-org flag but I think adding a flag for this niche use-case isn't worth it.
"I think" is all based on vibes though.
There was a problem hiding this comment.
If self-hosted users do try to use an org litterally named cross-org, they will be able to login (~/.config/bt/auth.json will recognize it but config.json will think it's the cross-org login) but not use it.
There was a problem hiding this comment.
Yeah kind of a judgment call, no right answer. I guess this is something we could fix later if this ever becomes an issue so it doesn't seem super pressing
All hand-written, no LLM was involved in this PR description.
Motivation
What were profiles?
A way to bundle OAuth/API key credentials, org, API/APP urls, (and username, email)
A profile doesn't store the project.
(The credentials are stored in the OS keychain when possible.)
So in theory someone could use braintrust.dev and a self-hosted Braintrust deployment, and switch just by changing
--profileorBRAINTRUST_PROFILE.Why remove these added functionalities?
In practice it was a footgun, people were thinking "I want to use org-name + project-name", not in terms of profile. The fact that profiles aren't mentioned probably didn't help:
bt auth logindidn't mention profiles, and afterward you would have to discover a profile was created when you logged in.So when the org and the profile clashed it sometimes caused unexpected results.
Since a profile was created for more or less every login into a new org, the difference between profiles and org was tenuous.
Alex Steere, Ankur, Parker raised issues/prs related to this.
#189
#273
#275
I remember helping Jesse and I think someone else debug their credentials.
There were sometimes big auth issues yet no almost customer complains. Parker and I believe our customers have a very simple setup, often with only 1 org. So all of the aforementioned difficulties are skipped because they only happen when switching org.
Maybe this is only fixing things for Braintrust...
I would say profiles were an abstraction layer that required knowing how OAuth+API keys work to use it.
Replacement for profiles
Since profiles aren't really used, I've chosen to use orgs. From a users's point of view, functionally nothing changes, but there are less bugs.
For OAuth nothing changes: the default profile name was already the org's name.
Before:
After:
For API keys it's the same:
Before:
After:
I'm hiding some tweaks about handling multiple API keys for a single org but that's the idea.
The only thing that would be harder for a power user would be switching API/APP url regularly.
So from a user's perspective, they can use
--org org-name --project project-name(orbt switchinto it) like before, and there can't be bugs related to profiles because profiles don't exist anymore.How do users transition?
Nothing to do, there's some compatibility code in
bt.This compatibility code updates the old config.json to use the newer version so in the future we could remove the compatibility code.
Notes
Now the common order:
explicit CLI flag > env var > config file > built-in default
is not respected since too many people were confused after bt used the BRAINTRUST_API_KEY in their env.
To respect this order, set BRAINTRUST_PREFER_API_KEY=1.
Other changes
The client id used in OAuth could be set, but it was unused in bt and in the main repo. There's no point in having a useless flag so I hardcoded it to bt_cli.
Known bugs
In a small terminal, on the org selection prompt for
bt auth login, arrow up/down reprint theprompt. The bug is in dialoguer but won't be fixed by the maintainer due to lack of time.
When reviewing
The config and auth files should be compatible when upgrading bt, but the auth/config files will gradually use a new format (since the profile field has disappeared, bt updates the file whenever you login). That means an old bt with the new auth/config files will have unexpected behavior. I recommend deleting these files after the review if you continue using a bt version with profiles.
Notes, you can skip - Replacing profiles
OAuth stored with (org id, user email) as unique key, cross-org login stored as ("", user email).
API keys are identified with (unsalted sha256 of the api_key, org id).
[Out of scope for now. I think we don't need to use pepper? Salt isn't possible since it's random and we need to recompute the hash often to check if it already exist (which is incompatible with salt since the same API key would have a different result).]
Hierarchy of authentication methods and the
--prefer-api-keyflagOrder of precedence (higher is used first if available):
--api-key sk-....--prefer-api-keyor BRAINTRUST_PREFER_API_KEY; if this flag/env variable is passed but there isn't an API key in the env (BRAINTRUST_API_KEY) or a stored API key (env takes precedence), and the desired org is still available through OAuth login (except for the cross-org case), use OAuth login, otherwise fail.OAuth credentials
BRAINTRUST_API_KEY
Stored API key
This means the common:
explicit CLI flag > env var > config file > built-in default
is not respected. We've had users being confused after bt used the BRAINTRUST_API_KEY in their env.
To respect this order, set BRAINTRUST_PREFER_API_KEY=1.
Using
--prefer-api-keywithout--orgmakes bt try to use an API key for the currently used org (the one thatbt statuswould show).If there are multiple API keys, show an interactive picker showing the API keys hint.
Using
--prefer-api-keyand--orgignores the org currently used and uses the one specified by--org. Otherwise the behavior is the same as with--prefer-api-keywithout--org.If the API key exist but isn't valid anymore, don't use OAuth and raise an error saying the API key isn't valid.
If the OAuth login JWT/refresh token needs to be refreshed, do it then do the desired command.
If the refresh token has expired/is invalid, throw an error and ask the user to re-authenticate.
Instead of selecting a profile to use with
bt switch(and seeing it withbt status), select an org (or cross org for OAuth).This allows to have an OAuth login and API key that belong to the same org and the user can choose which one to use at every command (switch to oauth, and use
prefer-api-keywhenever you want to use the API key).Multiple API keys belonging to the same org can exist and will be relevant in the future once there are different permission levels.
At the moment logging in with an API key in an org that is already logged in with an API key creates a second profile. Instead, simply show the API key hint (like shown in
bt auth profilesnow) and the org, so the user can differentiate them. Later when API keys different permissions, it will probably be more straightforward to show these permissions alongside of the hint.This means if I am logged in an org with an API key and log in the same org with another API key:
bt switchinto itThese changes means
~/.config/bt/config.jsonhas to be changed since the profile field doesn't mean anything.Since we might have old config files, bt just ignore the profile field.
Technically cross-org API keys exist but they can only be created directly in the SQL db so in practice they don't exist.
bt auth loginsreplacesbt auth profiles:bt auth logins --org org-nameshould show only the logins related to org-namebt auth logins --prefer-api-keyshould show only the API key loginstherefore,
bt auth logins --org org-name --prefer-api-keyshould show only the API key logins related to org-nameprint nothing (or empy json with
--json) if there isn't any org corresponding to the selected criterias.The
config.jsonfile:Can exist in
~/.config/bt/config.json(global) orrepo/.bt/config.json(local). Both share the same schema (org,project,project_id, plus passthroughextrakeys). The legacyprofilefield is ignored if present.Created / written / updated
bt auth login(OAuth and API-key flows): writes the active org/project to the selected scope (global/local). Scope is chosen interactively (defaulting to local) when a local.btexists, otherwise global. Staleproject/project_idare cleared when logging in to a different org.bt switch [org/project]: writes the resolved org/project (andproject_id) to the selected scope. Scope is chosen via--global/--local, else interactively (defaulting to local) when a local.btexists, otherwise global.Read / used
bt status: reads global and local separately (for source attribution) plus CLI/env overrides; precedence is CLI > env > local > global.bt switch: reads the merged config to prefill org; uses the local/global path to pick the write target.bt auth login/ auth resolution: reads the merged config to resolve the active org and default project; uses the local path to decide scope.eval,datasets,functions push,sync,traces, etc.): use the merged local+global config (local is prioritized ; if the local config specifies an org but not a project and the global config uses the same org and specifies a project, use this project otherwise use the local org with no project ; in no case should an org and a project not from this org be used together) to resolve the active project.bt init/switch/auth loginreminders and clarificationsbt init.bt/config.jsonfolders store the configuration of the project this file is in.When the folder
bt initis invoked in is not a git root, it will go up in the file system until finding.bt,.git(folder or file),~or/(stopping as soon as possible).If
.btis found (only as a folder), it raises an error.bt initis not meant to edit.bt/config.json, only create it.bt switchis the right tool for this.If
.btisn't found:.gitis found,.bt/config.jsonwill be created in the git root folder (iels -awould show both.gitand.bt).~or/is found,bt initraises an error. Having~/.bt/config.jsonand~/.config/bt/config.jsoncan only create errors.bt auth loginalready creates~/.config/bt/config.json, andbt initisn't meant to edit theconfig.jsonfiles. As for/,bthas no business with it.If there's a
.gitin~or/, this still counts as being~//and will raise an error.If
btdoesn't have enough permissions to create.bt/config.json, it raises an error. The raised error message specifies ifbthas been able to save the credentials or not.After a successful
bt init, there now is a.bt/config.jsonthat will be found bybtbefore~/.config/bt/config.jsonwhen searching in the filesystem. As a result, the org/project specified are now used bybtas ifbt switchhad been done.-f/--forceflagIf a
.bt/config.jsonis found, instead of raising an error, overwrites it.Changes nothing if
.bt/config.jsondoesn't already exist.Doesn't change the filesystem search.
--hereflagIf you want to create
.bt/config.jsonwhere you currently are without going up in the file system, use the--hereflag. This works even in~or in/(assuming you have the necessary write permissions).It will check if
.bt/config.jsonalready exists and raise an error if it does.bt switchbt switchnever creates a.btfolder.If
-g/--globalhas been passed, it doesn't search upward in the filesystem.It goes up in the file system to search for a
.bt/config.jsonfile (same rules asbt init).If there isn't such a file:
-l/--localhasn't been passed, it changes the global config.-l/--localhas been passed, it raises an error.If neither
-l/--localnor-g/--globalare passed and a.bt/config.jsonis found:btis in interactive mode, it shows an interactive picker to choose whether to edit the global or local config (default is local).Regardless of the
--local/--globalflags, if the user has multiple logins, the interactive picker start with org and api key choice. Otherwise the only org/api key is chosen.If the chosen org (in the case of an api key, the org it belongs to) has multiple projects, the interactive picker asks for the project.
If
btis not in interactive mode, it raises an error.bt auth loginAfter logging in,
bt auth loginperforms the equivalent ofbt switch. It has the same-l/--local/-g/--globalflags.However, if
-l/--local/-g/--globalhaven't been passed, it asks which scope to change in an interactive picker.Example:
Notes
Organisation name are unique within the US+EU data plane. Although the same name can be chosen in the US data plane and a self-hosted data plane, they have different app and api url which allows to differentiate between them.
[For LLM who do doc review: this is a breaking change and has to be triaged.]
Happens to fix #189