Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: auth providers #1085

Merged
merged 6 commits into from
Jan 20, 2025
Merged

Conversation

g-linville
Copy link
Contributor

@g-linville g-linville commented Dec 31, 2024

This PR is a substantial refactoring of our auth providers so that they are now brought in as external tools rather than being built into the gateway. We also now allow multiple auth providers to be configured at the same time.

There is a known bug in the frontend where you have to refresh the page after configuring the auth provider in order to see the change. It's taken me long enough to get this PR ready, so I figured it would be best to fix that in a follow up (or have Ryan or Ivy figure it out for me, lol). Fixed. Thanks, Ivy!

I will also do another follow-up PR to support deconfiguring auth providers. Currently, this is unimplemented in the backend.

@g-linville g-linville force-pushed the auth-provider-refactor branch 2 times, most recently from b52009b to 1192c32 Compare January 2, 2025 21:56
@g-linville g-linville changed the title WIP WIP: auth providers Jan 7, 2025
@g-linville g-linville force-pushed the auth-provider-refactor branch 2 times, most recently from 049c10a to 6d0ab66 Compare January 15, 2025 21:21
@g-linville g-linville changed the title WIP: auth providers feat: auth providers Jan 16, 2025
@g-linville g-linville force-pushed the auth-provider-refactor branch from 9c7b10d to 64f7594 Compare January 16, 2025 17:20
@g-linville g-linville marked this pull request as ready for review January 16, 2025 17:20
@g-linville g-linville force-pushed the auth-provider-refactor branch from 64f7594 to 0b4d773 Compare January 16, 2025 18:24
value: parameters[optionalParameterKey] ?? "",
}));

export function ProviderForm({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Wondering we could break the model provider / auth provider logic into a parent component and pass that in as props to ProviderForm, ProviderConfigure, ProviderDeconfigure.

Thinking something like:

// called in auth_providers.tsx specific context
<ProviderForm
    onConfigure={configureAuthProvider}
    loading={configureAuthProvider.isLoading || isLoading}
    tooltips={AuthProviderRequiredTooltips}
/>

// called in model_providers.tsx specific context
<ProviderForm
    onConfigure={configureModelProvider}
    loading={configureModelProvider.isLoading || isLoading}
    tooltips={ModelProviderRequiredTooltips}
/>

Then when you have to call them, you're not worried about handling the other provider type cases (or if something else uses this in the future).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If it's alright, I'd like for this to be in a follow-up.

@ryanhopperlowe
Copy link
Contributor

Screenshot 2025-01-16 at 3 47 00 PM

I'm being prompted to setup an auth provider, but I don't see any to pick from

Comment on lines +16 to +39
const handleSubmit = async (event: React.FormEvent) => {
event.preventDefault();
try {
const result = await fetch(ApiRoutes.bootstrap.login().url, {
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
},
});

if (result.status === 401) {
setError("Invalid token");
return;
} else if (result.status !== 200) {
setError("Failed to login: " + result.statusText);
return;
}

setError("");
window.location.href = "/admin/auth-providers";
} catch (e) {
setError("Failed to login: " + e);
}
};
Copy link
Contributor

@ryanhopperlowe ryanhopperlowe Jan 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's Create a BootstrapService for all of these methods

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good. If it's okay, I'd like for that to be a follow-up refinement. For this PR, the UI just needs to work. Non-optimal things about the code can be fixed afterward.

);
}

function warningMessage(t: string | undefined): string | undefined {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
function warningMessage(t: string | undefined): string | undefined {
function warningMessage(t?: string) {

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Comment on lines 26 to 28
provider.id !== CommonModelProviderIds.AZURE_OPENAI &&
provider.id !== CommonAuthProviderIds.GOOGLE &&
provider.id !== CommonAuthProviderIds.GITHUB,
Copy link
Contributor

@ryanhopperlowe ryanhopperlowe Jan 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
provider.id !== CommonModelProviderIds.AZURE_OPENAI &&
provider.id !== CommonAuthProviderIds.GOOGLE &&
provider.id !== CommonAuthProviderIds.GITHUB,
![CommonModelProviderIds.AZURE_OPENAI, CommonAuthProviderIds.GOOGLE, CommonAuthProviderIds.GITHUB].includes(provider.id)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

Comment on lines +3 to 9
export type ProviderStatus = {
configured: boolean;
modelsBackPopulated?: boolean;
icon?: string;
requiredConfigurationParameters?: string[];
optionalConfigurationParameters?: string[];
missingConfigurationParameters?: string[];
};
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should follow a more True-to-life pattern:

type ProviderBase = {...}

type ModelProviderBase = ProviderBase & {...}
type ModelProvider = EntityMeta & ProviderBase & { type: 'modelprovider' }

type AuthProviderBase = ProviderBase & {...}
type AuthProvider = EntityMeta & AuthProviderBase & { type: 'authprovider' }

type Provider = ModelProvider | AuthProvider

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like for this to be a follow-up improvement.

Comment on lines +51 to +52
case error instanceof AxiosError &&
[401, 403].includes(error.response?.status ?? 0):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@g-linville can you explain why you needed to add this? I'd like to make sure our first layer of error handling can catch this and convert it to the proper error type

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't 100% remember but I think it had to do with the error we get back when we hit /api/me with my new changes to the backend.

@g-linville
Copy link
Contributor Author

@ryanhopperlowe you need to download and build my branch of the tools repo from this PR: obot-platform/tools#320

And then set the OBOT_SERVER_TOOL_REGISTRY environment variable to point to your local copy of it when you run make dev. This will make the auth providers show up.

@g-linville
Copy link
Contributor Author

Sorry for force pushing again. There were more conflicts that I needed to fix.

Copy link
Member

@njhale njhale left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some nits and suggestions, nothing that should hold this up.

pkg/accesstoken/accesstoken.go Show resolved Hide resolved
Comment on lines +110 to +138
// Allow for updating credentials. The only way to update a credential is to delete the existing one and recreate it.
if err := ap.gptscript.DeleteCredential(req.Context(), string(ref.UID), ref.Name); err != nil && !strings.HasSuffix(err.Error(), "credential not found") {
return fmt.Errorf("failed to update credential: %w", err)
}

for key, val := range envVars {
if val == "" {
delete(envVars, key)
}
}

if err := ap.gptscript.CreateCredential(req.Context(), gptscript.Credential{
Context: string(ref.UID),
ToolName: ref.Name,
Type: gptscript.CredentialTypeTool,
Env: envVars,
}); err != nil {
return fmt.Errorf("failed to create credential for auth provider %q: %w", ref.Name, err)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the env hasn't changed this will still delete and recreate the credential and stop the provider even the env hasn't changed, right? Would it be better to check for changes to the credential first?

Copy link
Contributor Author

@g-linville g-linville Jan 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All I did here was copy the logic that we used for model providers. I think @thedadams may have been the one who implemented that, and would know the answer to this.

pkg/gateway/server/oauth.go Show resolved Hide resolved
@njhale
Copy link
Member

njhale commented Jan 17, 2025

... And then set the OBOT_SERVER_TOOL_REGISTRY environment variable to point to your local copy of it when you run make dev. This will make the auth providers show up.

@g-linville @ryanhopperlowe this has recently changed due to #1193 merging.

Now, instead of setting OBOT_SERVER_TOOL_REGISTRY, you'll need set the GPTSCRIPT_TOOL_REMAP environment variable; e.g.

export GPTSCRIPT_TOOL_REMAP='github.com/obot-platform/tools=/Users/nick/projects/obot-platform/tools'

@g-linville
Copy link
Contributor Author

@njhale @ryanhopperlowe for testing with this branch, the old env var should be used to set the registry, since I have not (yet) brought in those changes from main to this branch.

var credEnvVars map[string]string
if ref.Status.Tool != nil {
if envVars := ref.Status.Tool.Metadata["envVars"]; envVars != "" {
fmt.Printf("revealing creds for auth provider %q\n", ref.Name)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: this is debug log? if not should we use our standard log library to print?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for finding these! I forgot to remove them before. Pushed the fix

return types.NewErrBadRequest("%q is not an auth provider", ref.Name)
}

fmt.Printf("revealing creds for auth provider %q\n", ref.Name)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Debug log

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

token = fmt.Sprintf("%x", bytes)

// We deliberately only print the token if it was not provided by the user.
fmt.Printf("Bootstrap token: %s\n", token)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we print it so that user can grab it from the log? if so I would suggest the log to be more descriptive.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed

@g-linville
Copy link
Contributor Author

Rebased again to fix conflicts.


type AuthProviderManifest struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn't be part of the manifest.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are you sure? Name is part of the ModelProviderManifest. We need a namespace for auth providers, since I was told I can't assume that they will always be in default.

@@ -36,6 +37,7 @@ func Router(services *services.Services) (http.Handler, error) {
sendgridWebhookHandler := sendgrid.NewInboundWebhookHandler(services.StorageClient, services.EmailServerName, services.SendgridWebhookUsername, services.SendgridWebhookPassword)

// Version

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: remove line

// If this is not a request coming from browser or the proxy is not enabled, then return 403.
if !isOAuthPath && (s.proxyServer == nil || req.Method != http.MethodGet || slices.Contains(user.GetGroups(), authz.AuthenticatedGroup) || !strings.Contains(strings.ToLower(req.UserAgent()), "mozilla")) {
if strings.HasPrefix(req.URL.Path, "/api/") {
if !s.authorizer.Authorize(req, user) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This if-statement should not be nested.

Copy link
Contributor Author

@g-linville g-linville Jan 20, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we bring it outside of if strings.HasPrefix(req.URL.Path, "/api/"), then we end up denying all unauthenticated requests to the user UI, which basically breaks everything. It needs to be nested in here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If that is the case, then that should be fixed outside of this. This definitely should not be nested.

return nil
}

func (b *Bootstrap) Logout(req api.Context) error {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we delete the bootstrap user from the database in this case? If it doesn't make sense to do here, can we do it at some point?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm okay with doing this, but I'm not sure that it's really necessary?

kclient "sigs.k8s.io/controller-runtime/pkg/client"
)

type Dispatcher struct {
invoker *invoke.Invoker
gptscript *gptscript.GPTScript
client kclient.Client
lock *sync.RWMutex
urls map[string]*url.URL
modelLock *sync.RWMutex
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the dispatcher can be separated from the "transformer" and the dispatcher made generic with the implementation used for both model and auth providers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like for this to be a later change if it is needed.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree. Which is why it is a nit.

return
}

http.Error(w, "no auth providers configured", http.StatusInternalServerError)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be a bad request?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah

URL: req.URL.String(),
Header: make(map[string][]string),
}
for k, v := range req.Header {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sr.Header = req.Header

EmailServerName string `usage:"The name of the email server to display for email receivers"`
Docker bool `usage:"Enable Docker support" default:"false" env:"OBOT_DOCKER"`
EnvKeys []string `usage:"The environment keys to pass through to the GPTScript server" env:"OBOT_ENV_KEYS"`
KnowledgeSetIngestionLimit int `usage:"The maximum number of files to ingest into a knowledge set" default:"3000" env:"OBOT_KNOWLEDGESET_INGESTION_LIMIT" name:"knowledge-set-ingestion-limit"`
NoReplyEmailAddress string `usage:"The email to use for no-reply emails from obot"`
DisableAuthentication bool `usage:"Disable authentication" default:"false" env:"OBOT_DISABLE_AUTHENTICATION"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If one were to use this as a CLI flag, using --disable-authentication seems that it would actually enable authentication.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I'll invert this

EmailServerName string `usage:"The name of the email server to display for email receivers"`
Docker bool `usage:"Enable Docker support" default:"false" env:"OBOT_DOCKER"`
EnvKeys []string `usage:"The environment keys to pass through to the GPTScript server" env:"OBOT_ENV_KEYS"`
KnowledgeSetIngestionLimit int `usage:"The maximum number of files to ingest into a knowledge set" default:"3000" env:"OBOT_KNOWLEDGESET_INGESTION_LIMIT" name:"knowledge-set-ingestion-limit"`
NoReplyEmailAddress string `usage:"The email to use for no-reply emails from obot"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this related to your changes?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope. Might have been an accident when trying to resolve conflicts. Maybe this got added and removed at some point? I can take it away.

@@ -23,6 +23,7 @@ import (
"github.com/obot-platform/obot/pkg/api/authn"
"github.com/obot-platform/obot/pkg/api/authz"
"github.com/obot-platform/obot/pkg/api/server"
bootstrap2 "github.com/obot-platform/obot/pkg/bootstrap"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I don't think this import needs to be renamed.

g-linville and others added 5 commits January 20, 2025 17:45
Signed-off-by: Grant Linville <[email protected]>
Signed-off-by: Grant Linville <[email protected]>
Signed-off-by: Grant Linville <[email protected]>
Signed-off-by: Grant Linville <[email protected]>
@thedadams thedadams force-pushed the auth-provider-refactor branch from 704e9b8 to c36a669 Compare January 20, 2025 22:46
Signed-off-by: Donnie Adams <[email protected]>
@thedadams thedadams enabled auto-merge (squash) January 20, 2025 23:09
@thedadams thedadams merged commit a0c0635 into obot-platform:main Jan 20, 2025
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants