diff --git a/internal/auth/auth.go b/internal/auth/auth.go index b42af5af2..ef8786d35 100644 --- a/internal/auth/auth.go +++ b/internal/auth/auth.go @@ -116,6 +116,29 @@ func WaitUntilUserLogsIn(ctx context.Context, httpClient *http.Client, state Sta } } +var RequiredScopes = []string{ + "openid", + "offline_access", // for retrieving refresh token + "create:clients", "delete:clients", "read:clients", "update:clients", + "read:client_grants", + "create:resource_servers", "delete:resource_servers", "read:resource_servers", "update:resource_servers", + "create:roles", "delete:roles", "read:roles", "update:roles", + "create:rules", "delete:rules", "read:rules", "update:rules", + "create:users", "delete:users", "read:users", "update:users", + "read:branding", "update:branding", + "read:email_templates", "update:email_templates", + "read:email_provider", + "read:connections", "update:connections", + "read:client_keys", "read:logs", "read:tenant_settings", + "read:custom_domains", "create:custom_domains", "update:custom_domains", "delete:custom_domains", + "read:anomaly_blocks", "delete:anomaly_blocks", + "create:log_streams", "delete:log_streams", "read:log_streams", "update:log_streams", + "create:actions", "delete:actions", "read:actions", "update:actions", + "create:organizations", "delete:organizations", "read:organizations", "update:organizations", "read:organization_members", "read:organization_member_roles", "read:organization_connections", + "read:prompts", "update:prompts", + "read:attack_protection", "update:attack_protection", +} + // GetDeviceCode kicks-off the device authentication flow by requesting // a device code from Auth0. The returned state contains the // URI for the next step of the flow. @@ -212,7 +235,6 @@ func GetAccessTokenFromClientCreds(ctx context.Context, args ClientCredentials) TokenURL: u.String() + "/oauth/token", EndpointParams: url.Values{ "client_id": {args.ClientID}, - "scope": {strings.Join(RequiredScopesForClientCreds(), " ")}, "audience": {u.String() + "/api/v2/"}, }, } diff --git a/internal/auth/scopes.go b/internal/auth/scopes.go deleted file mode 100644 index a9d31dcfb..000000000 --- a/internal/auth/scopes.go +++ /dev/null @@ -1,37 +0,0 @@ -package auth - -var RequiredScopes = []string{ - "openid", - "offline_access", // for retrieving refresh token - "create:clients", "delete:clients", "read:clients", "update:clients", - "read:client_grants", - "create:resource_servers", "delete:resource_servers", "read:resource_servers", "update:resource_servers", - "create:roles", "delete:roles", "read:roles", "update:roles", - "create:rules", "delete:rules", "read:rules", "update:rules", - "create:users", "delete:users", "read:users", "update:users", - "read:branding", "update:branding", - "read:email_templates", "update:email_templates", - "read:email_provider", - "read:connections", "update:connections", - "read:client_keys", "read:logs", "read:tenant_settings", - "read:custom_domains", "create:custom_domains", "update:custom_domains", "delete:custom_domains", - "read:anomaly_blocks", "delete:anomaly_blocks", - "create:log_streams", "delete:log_streams", "read:log_streams", "update:log_streams", - "create:actions", "delete:actions", "read:actions", "update:actions", - "create:organizations", "delete:organizations", "read:organizations", "update:organizations", "read:organization_members", "read:organization_member_roles", "read:organization_connections", - "read:prompts", "update:prompts", - "read:attack_protection", "update:attack_protection", -} - -// RequiredScopesForClientCreds returns minimum scopes required when authenticating with client credentials. -func RequiredScopesForClientCreds() []string { - var min []string - for _, s := range RequiredScopes { - // Both "offline_access" and "openid" scopes only apply to device-flow authentication - // and should be ignored when authenticating with client credentials - if s != "offline_access" && s != "openid" { - min = append(min, s) - } - } - return min -} diff --git a/internal/cli/actions.go b/internal/cli/actions.go index a94f71653..a73019cf4 100644 --- a/internal/cli/actions.go +++ b/internal/cli/actions.go @@ -419,7 +419,7 @@ func deployActionCmd(cli *cli) *cobra.Command { auth0 actions deploy --json`, RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { - if err := actionID.Pick(cmd, &inputs.ID, cli.actionPickerOptions); err != nil { + if err := actionID.Pick(cmd, &inputs.ID, cli.undeployedActionPickerOptions); err != nil { return err } } else { @@ -498,6 +498,26 @@ func (c *cli) actionPickerOptions(ctx context.Context) (pickerOptions, error) { return opts, nil } +func (c *cli) undeployedActionPickerOptions(ctx context.Context) (pickerOptions, error) { + list, err := c.api.Action.List(ctx, management.Parameter("deployed", "false")) + if err != nil { + return nil, err + } + + var opts pickerOptions + for _, r := range list.Actions { + label := fmt.Sprintf("%s %s", r.GetName(), ansi.Faint("("+r.GetID()+")")) + + opts = append(opts, pickerOption{value: r.GetID(), label: label}) + } + + if len(opts) == 0 { + return nil, errors.New("There are currently no actions to deploy.") + } + + return opts, nil +} + func (c *cli) actionEditorHint() { c.renderer.Infof("%s Once you close the editor, the action will be saved. To cancel, press CTRL+C.", ansi.Faint("Hint:")) } diff --git a/internal/cli/actions_test.go b/internal/cli/actions_test.go index a091703cf..dee12f61b 100644 --- a/internal/cli/actions_test.go +++ b/internal/cli/actions_test.go @@ -211,6 +211,85 @@ func TestActionsPickerOptions(t *testing.T) { } } +func TestUndeployedActionsPickerOptions(t *testing.T) { + tests := []struct { + name string + actions []*management.Action + apiError error + assertOutput func(t testing.TB, options pickerOptions) + assertError func(t testing.TB, err error) + }{ + { + name: "happy path", + actions: []*management.Action{ + { + ID: auth0.String("some-id-1"), + Name: auth0.String("some-name-1"), + }, + { + ID: auth0.String("some-id-2"), + Name: auth0.String("some-name-2"), + }, + }, + assertOutput: func(t testing.TB, options pickerOptions) { + assert.Len(t, options, 2) + assert.Equal(t, "some-name-1 (some-id-1)", options[0].label) + assert.Equal(t, "some-id-1", options[0].value) + assert.Equal(t, "some-name-2 (some-id-2)", options[1].label) + assert.Equal(t, "some-id-2", options[1].value) + }, + assertError: func(t testing.TB, err error) { + t.Fail() + }, + }, + { + name: "no actions", + actions: []*management.Action{}, + assertOutput: func(t testing.TB, options pickerOptions) { + t.Fail() + }, + assertError: func(t testing.TB, err error) { + assert.ErrorContains(t, err, "There are currently no actions to deploy.") + }, + }, + { + name: "API error", + apiError: errors.New("error"), + assertOutput: func(t testing.TB, options pickerOptions) { + t.Fail() + }, + assertError: func(t testing.TB, err error) { + assert.Error(t, err) + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + actionAPI := mock.NewMockActionAPI(ctrl) + actionAPI.EXPECT(). + List(gomock.Any(), gomock.Any()). + Return(&management.ActionList{ + Actions: test.actions}, test.apiError) + + cli := &cli{ + api: &auth0.API{Action: actionAPI}, + } + + options, err := cli.undeployedActionPickerOptions(context.Background()) + + if err != nil { + test.assertError(t, err) + } else { + test.assertOutput(t, options) + } + }) + } +} + func TestActionsInputSecretsToActionSecrets(t *testing.T) { t.Run("it should map input secrets to action payload", func(t *testing.T) { input := map[string]string{ diff --git a/internal/cli/organizations.go b/internal/cli/organizations.go index 691024a0c..ba5192eb3 100644 --- a/internal/cli/organizations.go +++ b/internal/cli/organizations.go @@ -523,8 +523,7 @@ func listMembersOrganizationCmd(cli *cli) *cobra.Command { } if len(args) == 0 { - err := organizationID.Pick(cmd, &inputs.ID, cli.organizationPickerOptions) - if err != nil { + if err := organizationID.Pick(cmd, &inputs.ID, cli.organizationPickerOptions); err != nil { return err } } else { @@ -535,8 +534,11 @@ func listMembersOrganizationCmd(cli *cli) *cobra.Command { if err != nil { return err } + sortMembers(members) + cli.renderer.MembersList(members) + return nil }, } @@ -584,9 +586,9 @@ func listRolesOrganizationCmd(cli *cli) *cobra.Command { if inputs.Number < 1 || inputs.Number > 1000 { return fmt.Errorf("number flag invalid, please pass a number between 1 and 1000") } + if len(args) == 0 { - err := organizationID.Pick(cmd, &inputs.OrgID, cli.organizationPickerOptions) - if err != nil { + if err := organizationID.Pick(cmd, &inputs.OrgID, cli.organizationPickerOptions); err != nil { return err } } else { @@ -597,12 +599,16 @@ func listRolesOrganizationCmd(cli *cli) *cobra.Command { if err != nil { return err } + roleMap, err := cli.getOrgMemberRolesWithSpinner(cmd.Context(), inputs.OrgID, members) if err != nil { return err } + roles := cli.convertOrgRolesToManagementRoles(roleMap) + cli.renderer.RoleList(roles) + return nil }, } @@ -775,15 +781,17 @@ func (cli *cli) getOrgMembers( output = append(output, member) } return output, members.HasNext(), nil - }) - + }, + ) if err != nil { - return nil, fmt.Errorf("Unable to list members of an organization with ID '%s': %w", orgID, err) + return nil, fmt.Errorf("failed to list members of an organization with ID %q: %w", orgID, err) } + var typedList []management.OrganizationMember for _, item := range list { typedList = append(typedList, item.(management.OrganizationMember)) } + return typedList, nil } @@ -796,24 +804,28 @@ func sortMembers(members []management.OrganizationMember) { func (cli *cli) getOrgMembersWithSpinner(context context.Context, orgID string, number int, ) ([]management.OrganizationMember, error) { var members []management.OrganizationMember - err := ansi.Spinner("Getting members of organization", func() error { - var errInner error - members, errInner = cli.getOrgMembers(context, orgID, number) - return errInner + + err := ansi.Waiting(func() (err error) { + members, err = cli.getOrgMembers(context, orgID, number) + return err }) + return members, err } func (cli *cli) getOrgMemberRolesWithSpinner(ctx context.Context, orgID string, members []management.OrganizationMember, ) (map[string]management.OrganizationMemberRole, error) { roleMap := make(map[string]management.OrganizationMemberRole) - err := ansi.Spinner("Getting roles for each member", func() error { + + err := ansi.Waiting(func() (err error) { for _, member := range members { userID := member.GetUserID() - roleList, errInner := cli.api.Organization.MemberRoles(ctx, orgID, userID) - if errInner != nil { - return errInner + + roleList, err := cli.api.Organization.MemberRoles(ctx, orgID, userID) + if err != nil { + return err } + for _, role := range roleList.Roles { roleID := role.GetID() if _, exists := roleMap[roleID]; !exists { @@ -821,8 +833,10 @@ func (cli *cli) getOrgMemberRolesWithSpinner(ctx context.Context, orgID string, } } } + return nil }) + return roleMap, err } @@ -830,24 +844,37 @@ func (cli *cli) convertOrgRolesToManagementRoles(roleMap map[string]management.O ) []*management.Role { var roles []*management.Role for _, role := range roleMap { - roles = append(roles, &management.Role{ID: role.ID, Name: role.Name, Description: role.Description}) + roles = append(roles, &management.Role{ + ID: role.ID, + Name: role.Name, + Description: role.Description, + }) } + sort.Slice(roles, func(i, j int) bool { return strings.ToLower(roles[i].GetName()) < strings.ToLower(roles[j].GetName()) }) + return roles } -func (cli *cli) getOrgRoleMembersWithSpinner(ctx context.Context, orgID string, roleID string, members []management.OrganizationMember, +func (cli *cli) getOrgRoleMembersWithSpinner( + ctx context.Context, + orgID string, + roleID string, + members []management.OrganizationMember, ) ([]management.OrganizationMember, error) { var roleMembers []management.OrganizationMember - errSpinner := ansi.Spinner("Getting roles assigned to organization members", func() error { + + err := ansi.Waiting(func() (err error) { for _, member := range members { userID := member.GetUserID() + roleList, err := cli.api.Organization.MemberRoles(ctx, orgID, userID) if err != nil { return err } + for _, role := range roleList.Roles { id := role.GetID() if id == roleID { @@ -855,7 +882,9 @@ func (cli *cli) getOrgRoleMembersWithSpinner(ctx context.Context, orgID string, } } } + return nil }) - return roleMembers, errSpinner + + return roleMembers, err } diff --git a/internal/cli/roles.go b/internal/cli/roles.go index 14d454897..96e25e420 100644 --- a/internal/cli/roles.go +++ b/internal/cli/roles.go @@ -25,10 +25,11 @@ var ( IsRequired: true, } roleDescription = Flag{ - Name: "Description", - LongForm: "description", - ShortForm: "d", - Help: "Description of the role.", + Name: "Description", + LongForm: "description", + ShortForm: "d", + Help: "Description of the role.", + IsRequired: true, } roleNumber = Flag{ Name: "Number", @@ -229,46 +230,46 @@ func updateRoleCmd(cli *cli) *cobra.Command { auth0 roles update -n myrole -d "awesome role" --json`, RunE: func(cmd *cobra.Command, args []string) error { if len(args) == 0 { - err := roleID.Pick(cmd, &inputs.ID, cli.rolePickerOptions) - if err != nil { + if err := roleID.Pick(cmd, &inputs.ID, cli.rolePickerOptions); err != nil { return err } } else { inputs.ID = args[0] } - // Prompt for role name - if err := roleName.AskU(cmd, &inputs.Name, nil); err != nil { + var currentRole *management.Role + if err := ansi.Waiting(func() (err error) { + currentRole, err = cli.api.Role.Read(cmd.Context(), inputs.ID) return err + }); err != nil { + return fmt.Errorf("failed to find role with ID %q: %v", inputs.ID, err) } - // Prompt for role description - if err := roleDescription.AskU(cmd, &inputs.Description, nil); err != nil { + if err := roleName.AskU(cmd, &inputs.Name, currentRole.Name); err != nil { return err } - // Start with an empty role object. We'll conditionally - // hydrate it based on the provided parameters since - // we'll do PATCH semantics. - r := &management.Role{} + if err := roleDescription.AskU(cmd, &inputs.Description, currentRole.Description); err != nil { + return err + } + + updatedRole := &management.Role{} if inputs.Name != "" { - r.Name = &inputs.Name + updatedRole.Name = &inputs.Name } - if inputs.Description != "" { - r.Description = &inputs.Description + updatedRole.Description = &inputs.Description } - // Update role if err := ansi.Waiting(func() error { - return cli.api.Role.Update(cmd.Context(), inputs.ID, r) + return cli.api.Role.Update(cmd.Context(), inputs.ID, updatedRole) }); err != nil { - return fmt.Errorf("Unable to update role: %v", err) + return fmt.Errorf("failed to update role with ID %q: %v", inputs.ID, err) } - // Render role creation specific view - cli.renderer.RoleUpdate(r) + cli.renderer.RoleUpdate(updatedRole) + return nil }, } diff --git a/internal/config/tenant.go b/internal/config/tenant.go index 1e30fe4c1..fa00cf49c 100644 --- a/internal/config/tenant.go +++ b/internal/config/tenant.go @@ -151,7 +151,7 @@ func (t *Tenant) RegenerateAccessToken(ctx context.Context) error { t.ExpiresAt = time.Now().Add(time.Duration(tokenResponse.ExpiresIn) * time.Second) } - if err := keyring.StoreAccessToken(t.Domain, t.AccessToken); err != nil { + if err := keyring.StoreAccessToken(t.Domain, t.AccessToken); err == nil { t.AccessToken = "" }