-
Notifications
You must be signed in to change notification settings - Fork 111
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
APP-7001: Add UpdateOauthApp CLI command #4641
base: main
Are you sure you want to change the base?
Changes from all commits
b842a80
5ff5353
a7cb473
3b461aa
6680cbd
ec0b6ad
b1c6350
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ import ( | |
"strings" | ||
|
||
"github.com/urfave/cli/v2" | ||
apppb "go.viam.com/api/app/v1" | ||
) | ||
|
||
// CLI flags. | ||
|
@@ -124,12 +125,24 @@ const ( | |
|
||
packageMetadataFlagFramework = "model_framework" | ||
|
||
// TODO: APP-6993 remove these flags. | ||
authApplicationFlagName = "application-name" | ||
authApplicationFlagApplicationID = "application-id" | ||
authApplicationFlagOriginURIs = "origin-uris" | ||
authApplicationFlagRedirectURIs = "redirect-uris" | ||
authApplicationFlagLogoutURI = "logout-uri" | ||
|
||
oauthAppFlagClientID = "client-id" | ||
oauthAppFlagClientName = "client-name" | ||
oauthAppFlagClientAuthentication = "client-authentication" | ||
oauthAppFlagPKCE = "pkce" | ||
oauthAppFlagEnabledGrants = "enabled-grants" | ||
oauthAppFlagURLValidation = "url-validation" | ||
oauthAppFlagOriginURIs = "origin-uris" | ||
oauthAppFlagRedirectURIs = "redirect-uris" | ||
oauthAppFlagLogoutURI = "logout-uri" | ||
unspecified = "unspecified" | ||
|
||
cpFlagRecursive = "recursive" | ||
cpFlagPreserve = "preserve" | ||
|
||
|
@@ -424,6 +437,82 @@ var app = &cli.App{ | |
Usage: "work with organizations", | ||
HideHelpCommand: true, | ||
Subcommands: []*cli.Command{ | ||
{ | ||
Name: "auth-service", | ||
Usage: "manage auth-service", | ||
Subcommands: []*cli.Command{ | ||
{ | ||
Name: "oauth-app", | ||
Usage: "manage the OAuth applications for an organization", | ||
Subcommands: []*cli.Command{ | ||
{ | ||
Name: "update", | ||
Usage: "update an OAuth application", | ||
Flags: []cli.Flag{ | ||
&cli.StringFlag{ | ||
Name: generalFlagOrgID, | ||
Required: true, | ||
Usage: "organization ID that is tied to the OAuth application", | ||
}, | ||
&cli.StringFlag{ | ||
Name: oauthAppFlagClientID, | ||
Usage: "id for the OAuth application to be updated", | ||
Required: true, | ||
}, | ||
&cli.StringFlag{ | ||
Name: oauthAppFlagClientName, | ||
Usage: "updated name for the OAuth application", | ||
Required: false, | ||
}, | ||
&cli.StringFlag{ | ||
Name: oauthAppFlagClientAuthentication, | ||
Usage: "updated client authentication policy for the OAuth application. can be one of " + | ||
allEnumValues(clientAuthenticationPrefix, apppb.ClientAuthentication_value), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please provide an example of what this would look like in the description so we know what these values mean and look like. This applies to all the other flags that also have a similar setup. |
||
Required: false, | ||
Value: unspecified, | ||
}, | ||
&cli.StringFlag{ | ||
Name: oauthAppFlagURLValidation, | ||
Usage: "updated url validation for the OAuth application. can be one of " + | ||
allEnumValues(urlValidationPrefix, apppb.URLValidation_value), | ||
Required: false, | ||
Value: unspecified, | ||
}, | ||
&cli.StringFlag{ | ||
Name: oauthAppFlagPKCE, | ||
Usage: "updated pkce for the OAuth application. can be one of " + | ||
allEnumValues(pkcePrefix, apppb.PKCE_value), | ||
Required: false, | ||
Value: unspecified, | ||
}, | ||
&cli.StringSliceFlag{ | ||
Name: oauthAppFlagOriginURIs, | ||
Usage: "updated comma separated origin uris for the OAuth application", | ||
Required: false, | ||
}, | ||
&cli.StringSliceFlag{ | ||
Name: oauthAppFlagRedirectURIs, | ||
Usage: "updated comma separated redirect uris for the OAuth application", | ||
Required: false, | ||
}, | ||
&cli.StringFlag{ | ||
Name: oauthAppFlagLogoutURI, | ||
Usage: "updated logout uri for the OAuth application", | ||
Required: false, | ||
}, | ||
&cli.StringSliceFlag{ | ||
Name: oauthAppFlagEnabledGrants, | ||
Usage: "updated comma separated enabled grants for the OAuth application. values can be of " + | ||
allEnumValues(enabledGrantPrefix, apppb.EnabledGrant_value), | ||
Required: false, | ||
}, | ||
}, | ||
Action: createCommandWithT[updateOAuthAppArgs](UpdateOAuthAppAction), | ||
}, | ||
}, | ||
}, | ||
}, | ||
}, | ||
{ | ||
Name: "list", | ||
Usage: "list organizations for the current user", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,6 +14,7 @@ import ( | |
"os/signal" | ||
"path/filepath" | ||
"runtime/debug" | ||
"slices" | ||
"strings" | ||
"time" | ||
|
||
|
@@ -2050,3 +2051,135 @@ func logEntryFieldsToString(fields []*structpb.Struct) (string, error) { | |
} | ||
return message + "}", nil | ||
} | ||
|
||
type updateOAuthAppArgs struct { | ||
OrgID string | ||
ClientID string | ||
ClientName string | ||
ClientAuthentication string | ||
Pkce string | ||
LogoutURI string | ||
UrlValidation string //nolint:revive,stylecheck | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @stuqdog is this ok? linter wants it to be URLValidation, but There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ooh yeah, that's a bit of a gotcha. I think what you did is correct, but thanks for flagging it! I'll file a ticket to revisit our struct parsing logic to see how we can avoid that in the future. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
OriginURIs []string | ||
RedirectURIs []string | ||
EnabledGrants []string | ||
} | ||
|
||
const ( | ||
clientAuthenticationPrefix = "CLIENT_AUTHENTICATION_" | ||
pkcePrefix = "PKCE_" | ||
urlValidationPrefix = "URL_VALIDATION_" | ||
enabledGrantPrefix = "ENABLED_GRANT_" | ||
) | ||
|
||
// allEnumValues returns the possible values we accept for a given proto enum. | ||
func allEnumValues(prefixToTrim string, enumValueMap map[string]int32) string { | ||
var formattedValues []string | ||
for values := range enumValueMap { | ||
formattedValue := strings.ToLower(strings.TrimPrefix(values, prefixToTrim)) | ||
formattedValues = append(formattedValues, formattedValue) | ||
} | ||
slices.Sort(formattedValues) | ||
return "[" + strings.Join(formattedValues, ", ") + "]" | ||
} | ||
Comment on lines
+2068
to
+2084
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. At this point, I'd just make shadow types to wrap around the proto enum types. You can look at the clients in |
||
|
||
// UpdateOAuthAppAction is the corresponding action for 'oauth-app update'. | ||
func UpdateOAuthAppAction(c *cli.Context, args updateOAuthAppArgs) error { | ||
client, err := newViamClient(c) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
return client.updateOAuthAppAction(c, args) | ||
} | ||
|
||
func (c *viamClient) updateOAuthAppAction(cCtx *cli.Context, args updateOAuthAppArgs) error { | ||
if err := c.ensureLoggedIn(); err != nil { | ||
return err | ||
} | ||
|
||
req, err := createUpdateOAuthAppRequest(args) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
_, err = c.client.UpdateOAuthApp(c.c.Context, req) | ||
if err != nil { | ||
return err | ||
} | ||
|
||
printf(cCtx.App.Writer, "Successfully updated OAuth app %s", args.ClientID) | ||
return nil | ||
} | ||
|
||
func createUpdateOAuthAppRequest(args updateOAuthAppArgs) (*apppb.UpdateOAuthAppRequest, error) { | ||
orgID := args.OrgID | ||
clientID := args.ClientID | ||
clientName := args.ClientName | ||
clientAuthentication := args.ClientAuthentication | ||
urlValidation := args.UrlValidation | ||
pkce := args.Pkce | ||
originURIs := args.OriginURIs | ||
redirectURIs := args.RedirectURIs | ||
logoutURI := args.LogoutURI | ||
enabledGrants := args.EnabledGrants | ||
|
||
clientAuthenticationEnum, ok := apppb.ClientAuthentication_value[clientAuthenticationPrefix+strings.ToUpper(clientAuthentication)] | ||
if !ok { | ||
return nil, errors.Errorf("--%s must be a valid ClientAuthentication, got %s. "+ | ||
"See `viam organizations auth-service oauth-app update --help` for supported options", | ||
oauthAppFlagClientAuthentication, clientAuthentication) | ||
} | ||
Comment on lines
+2127
to
+2132
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Similar to what my above comment, I would just use a shadow type with |
||
|
||
pkceEnum, ok := apppb.PKCE_value[pkcePrefix+strings.ToUpper(pkce)] | ||
if !ok { | ||
return nil, errors.Errorf("--%s must be a valid PKCE, got %s. "+ | ||
"See `viam organizations auth-service oauth-app update --help` for supported options", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 👍 |
||
oauthAppFlagPKCE, pkce) | ||
} | ||
|
||
urlValidationEnum, ok := apppb.URLValidation_value[urlValidationPrefix+strings.ToUpper(urlValidation)] | ||
if !ok { | ||
return nil, errors.Errorf("--%s must be a valid UrlValidation, got %s. "+ | ||
"See `viam organizations auth-service oauth-app update --help` for supported options", | ||
oauthAppFlagURLValidation, urlValidation) | ||
} | ||
|
||
egProto, err := enabledGrantsToProto(enabledGrants) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
req := &apppb.UpdateOAuthAppRequest{ | ||
OrgId: orgID, | ||
ClientId: clientID, | ||
ClientName: clientName, | ||
OauthConfig: &apppb.OAuthConfig{ | ||
ClientAuthentication: apppb.ClientAuthentication(clientAuthenticationEnum), | ||
Pkce: apppb.PKCE(pkceEnum), | ||
UrlValidation: apppb.URLValidation(urlValidationEnum), | ||
OriginUris: originURIs, | ||
RedirectUris: redirectURIs, | ||
LogoutUri: logoutURI, | ||
EnabledGrants: egProto, | ||
}, | ||
} | ||
return req, nil | ||
} | ||
|
||
func enabledGrantsToProto(enabledGrants []string) ([]apppb.EnabledGrant, error) { | ||
if enabledGrants == nil { | ||
return nil, nil | ||
} | ||
enabledGrantsProto := make([]apppb.EnabledGrant, len(enabledGrants)) | ||
for i, eg := range enabledGrants { | ||
enum, ok := apppb.EnabledGrant_value[enabledGrantPrefix+strings.ToUpper(eg)] | ||
if !ok { | ||
return nil, errors.Errorf("%s must consist of valid EnabledGrants, got %s. "+ | ||
"See `viam organizations auth-service oauth-app update --help` for supported options", | ||
oauthAppFlagEnabledGrants, eg) | ||
} | ||
enabledGrantsProto[i] = apppb.EnabledGrant(enum) | ||
} | ||
return enabledGrantsProto, nil | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1001,3 +1001,65 @@ func TestShellFileCopy(t *testing.T) { | |
}) | ||
}) | ||
} | ||
|
||
func TestUpdateOAuthAppAction(t *testing.T) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please add cases which check that passing in bad values to each of these will result in a specific error message |
||
updateOAuthAppFunc := func(ctx context.Context, in *apppb.UpdateOAuthAppRequest, | ||
opts ...grpc.CallOption, | ||
) (*apppb.UpdateOAuthAppResponse, error) { | ||
return &apppb.UpdateOAuthAppResponse{}, nil | ||
} | ||
asc := &inject.AppServiceClient{ | ||
UpdateOAuthAppFunc: updateOAuthAppFunc, | ||
} | ||
|
||
t.Run("valid inputs", func(t *testing.T) { | ||
flags := make(map[string]any) | ||
flags[generalFlagOrgID] = "org-id" | ||
flags[oauthAppFlagClientID] = "client-id" | ||
flags[oauthAppFlagClientName] = "client-name" | ||
flags[oauthAppFlagClientAuthentication] = "required" | ||
flags[oauthAppFlagURLValidation] = "allow_wildcards" | ||
flags[oauthAppFlagPKCE] = "not_required" | ||
flags[oauthAppFlagOriginURIs] = []string{"https://woof.com/login", "https://arf.com/"} | ||
flags[oauthAppFlagRedirectURIs] = []string{"https://woof.com/home", "https://arf.com/home"} | ||
flags[oauthAppFlagLogoutURI] = "https://woof.com/logout" | ||
flags[oauthAppFlagEnabledGrants] = []string{"implicit", "password"} | ||
cCtx, ac, out, errOut := setup(asc, nil, nil, nil, flags, "token") | ||
test.That(t, ac.updateOAuthAppAction(cCtx, parseStructFromCtx[updateOAuthAppArgs](cCtx)), test.ShouldBeNil) | ||
test.That(t, len(errOut.messages), test.ShouldEqual, 0) | ||
test.That(t, out.messages[0], test.ShouldContainSubstring, "Successfully updated OAuth app") | ||
}) | ||
|
||
t.Run("should error if client-authentication is not a valid enum value", func(t *testing.T) { | ||
flags := make(map[string]any) | ||
flags[generalFlagOrgID] = "org-id" | ||
flags[oauthAppFlagClientID] = "client-id" | ||
flags[oauthAppFlagClientAuthentication] = "not_one_of_the_allowed_values" | ||
cCtx, ac, out, _ := setup(asc, nil, nil, nil, flags, "token") | ||
err := ac.updateOAuthAppAction(cCtx, parseStructFromCtx[updateOAuthAppArgs](cCtx)) | ||
test.That(t, err, test.ShouldNotBeNil) | ||
test.That(t, err.Error(), test.ShouldContainSubstring, "client-authentication must be a valid ClientAuthentication") | ||
test.That(t, len(out.messages), test.ShouldEqual, 0) | ||
}) | ||
|
||
t.Run("should error if url-validation is not a valid enum value", func(t *testing.T) { | ||
flags := map[string]any{oauthAppFlagClientAuthentication: unspecified, oauthAppFlagPKCE: "not_one_of_the_allowed_values"} | ||
cCtx, ac, out, _ := setup(asc, nil, nil, nil, flags, "token") | ||
err := ac.updateOAuthAppAction(cCtx, parseStructFromCtx[updateOAuthAppArgs](cCtx)) | ||
test.That(t, err, test.ShouldNotBeNil) | ||
test.That(t, err.Error(), test.ShouldContainSubstring, "pkce must be a valid PKCE") | ||
test.That(t, len(out.messages), test.ShouldEqual, 0) | ||
}) | ||
|
||
t.Run("should error if url-validation is not a valid enum value", func(t *testing.T) { | ||
flags := map[string]any{ | ||
oauthAppFlagClientAuthentication: unspecified, oauthAppFlagPKCE: unspecified, | ||
oauthAppFlagURLValidation: "not_one_of_the_allowed_values", | ||
} | ||
cCtx, ac, out, _ := setup(asc, nil, nil, nil, flags, "token") | ||
err := ac.updateOAuthAppAction(cCtx, parseStructFromCtx[updateOAuthAppArgs](cCtx)) | ||
test.That(t, err, test.ShouldNotBeNil) | ||
test.That(t, err.Error(), test.ShouldContainSubstring, "url-validation must be a valid UrlValidation") | ||
test.That(t, len(out.messages), test.ShouldEqual, 0) | ||
}) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Look into why we are adding this dependency now (I would think this would have existed already and adding it is a mistake).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Triple check to confirm the dependency on API is okay -- even though inherently this stuff is "dependent" on the API I feel like this is a red herring.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Starting to see that this may be okay - it is imported already in this package (only in a different file) - the real question becomes whether we want to hardcode the string values that we import here instead of importing them - to maybe check against breaking changes in our API ... ? Again - deferring to someone who owns this code path
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see an actual issue with this, but it is a bit jarring to see a proto being imported here