Address SendGrid connector review feedback
- Add SendGrid third-party logo and wire it into ThirdPartyLogo - Add SendGrid name resolver (account company name, graceful fallback) - Fix MFA detection: full-access teammates carry both 2fa_exempt and 2fa_required, so report Unknown unless exactly one is present - Re-record the driver cassette against the live API - Use a random time suffix for the migration filename Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com>
This commit is contained in:
21
packages/ui/src/Atoms/ThirdParties/SendGrid.tsx
Normal file
21
packages/ui/src/Atoms/ThirdParties/SendGrid.tsx
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import type { ComponentProps } from "react";
|
||||||
|
|
||||||
|
export function SendGrid(props: ComponentProps<"svg">) {
|
||||||
|
return (
|
||||||
|
<svg
|
||||||
|
viewBox="0 0 256 256"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M256 0v170.667h-85.333v85.329H0V170.666H0V85.331h85.333V0H256Z"
|
||||||
|
fill="#9DD6E3"
|
||||||
|
/>
|
||||||
|
<polygon fill="#3F72AB" points="0 255.996 85.335 255.996 85.335 170.663 0 170.663" />
|
||||||
|
<polygon fill="#00A9D1" points="170.667 170.667 256 170.667 256 85.331 170.667 85.331" />
|
||||||
|
<polygon fill="#00A9D1" points="85.333 85.333 170.667 85.333 170.667 0 85.333 0" />
|
||||||
|
<polygon fill="#2191C4" points="85.333 170.665 170.667 170.665 170.667 85.331 85.333 85.331" />
|
||||||
|
<polygon fill="#3F72AB" points="170.667 85.333 256 85.333 256 0 170.667 0" />
|
||||||
|
</svg>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -42,6 +42,7 @@ import { OpenAI } from "./OpenAI";
|
|||||||
import { PagerDuty } from "./PagerDuty";
|
import { PagerDuty } from "./PagerDuty";
|
||||||
import { PostHog } from "./PostHog";
|
import { PostHog } from "./PostHog";
|
||||||
import { Resend } from "./Resend";
|
import { Resend } from "./Resend";
|
||||||
|
import { SendGrid } from "./SendGrid";
|
||||||
import { Sentry } from "./Sentry";
|
import { Sentry } from "./Sentry";
|
||||||
import { Slack } from "./Slack";
|
import { Slack } from "./Slack";
|
||||||
import { Supabase } from "./Supabase";
|
import { Supabase } from "./Supabase";
|
||||||
@@ -81,6 +82,7 @@ const thirdParties: Record<string, FC<ComponentProps<"svg">>> = {
|
|||||||
PAGERDUTY: PagerDuty,
|
PAGERDUTY: PagerDuty,
|
||||||
POSTHOG: PostHog,
|
POSTHOG: PostHog,
|
||||||
RESEND: Resend,
|
RESEND: Resend,
|
||||||
|
SENDGRID: SendGrid,
|
||||||
SENTRY: Sentry,
|
SENTRY: Sentry,
|
||||||
SLACK: Slack,
|
SLACK: Slack,
|
||||||
SUPABASE: Supabase,
|
SUPABASE: Supabase,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ export { OpenAI } from "./OpenAI";
|
|||||||
export { PagerDuty } from "./PagerDuty";
|
export { PagerDuty } from "./PagerDuty";
|
||||||
export { PostHog } from "./PostHog";
|
export { PostHog } from "./PostHog";
|
||||||
export { Resend } from "./Resend";
|
export { Resend } from "./Resend";
|
||||||
|
export { SendGrid } from "./SendGrid";
|
||||||
export { Sentry } from "./Sentry";
|
export { Sentry } from "./Sentry";
|
||||||
export { Slack } from "./Slack";
|
export { Slack } from "./Slack";
|
||||||
export { Supabase } from "./Supabase";
|
export { Supabase } from "./Supabase";
|
||||||
|
|||||||
@@ -486,6 +486,54 @@ func (r *anthropicNameResolver) ResolveInstanceName(ctx context.Context) (string
|
|||||||
return resp.Name, nil
|
return resp.Name, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sendGridNameResolver resolves the SendGrid account's company name from
|
||||||
|
// the user profile endpoint, used as the AccessSource instance label.
|
||||||
|
type sendGridNameResolver struct {
|
||||||
|
httpClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSendGridNameResolver(httpClient *http.Client) NameResolver {
|
||||||
|
return &sendGridNameResolver{httpClient: httpClient}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *sendGridNameResolver) ResolveInstanceName(ctx context.Context) (string, error) {
|
||||||
|
req, err := http.NewRequestWithContext(
|
||||||
|
ctx,
|
||||||
|
http.MethodGet,
|
||||||
|
"https://api.sendgrid.com/v3/user/profile",
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("cannot create sendgrid profile request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Accept", "application/json")
|
||||||
|
|
||||||
|
httpResp, err := r.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("cannot execute sendgrid profile request: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func() { _ = httpResp.Body.Close() }()
|
||||||
|
|
||||||
|
// Best-effort: a non-2xx (revoked key, or a key without the
|
||||||
|
// user.profile.read scope) must not make the source-name worker retry
|
||||||
|
// forever. Give up gracefully and keep the generic source name; a dead
|
||||||
|
// key surfaces on the next ListAccounts.
|
||||||
|
if httpResp.StatusCode < 200 || httpResp.StatusCode >= 300 {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var resp struct {
|
||||||
|
Company string `json:"company"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil {
|
||||||
|
return "", fmt.Errorf("cannot decode sendgrid profile response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resp.Company, nil
|
||||||
|
}
|
||||||
|
|
||||||
// sentryNameResolver resolves the Sentry organization name.
|
// sentryNameResolver resolves the Sentry organization name.
|
||||||
type sentryNameResolver struct {
|
type sentryNameResolver struct {
|
||||||
httpClient *http.Client
|
httpClient *http.Client
|
||||||
|
|||||||
@@ -212,15 +212,31 @@ func sendGridRole(userType string, isAdmin bool) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sendGridMFAStatus derives a teammate's MFA status from the auto-set 2fa
|
||||||
|
// scopes SendGrid attaches to the teammate detail. A restricted teammate
|
||||||
|
// carries exactly one of them to reflect their real status. Full-access
|
||||||
|
// users (the account owner and full-access teammates) are the exception:
|
||||||
|
// their scope list is the entire catalog and therefore contains BOTH
|
||||||
|
// 2fa_exempt and 2fa_required, which says nothing about their actual MFA.
|
||||||
|
// Only report a definitive status when exactly one scope is present;
|
||||||
|
// both-or-neither is ambiguous, so report Unknown rather than guessing.
|
||||||
func sendGridMFAStatus(scopes []string) coredata.MFAStatus {
|
func sendGridMFAStatus(scopes []string) coredata.MFAStatus {
|
||||||
|
var exempt, required bool
|
||||||
for _, scope := range scopes {
|
for _, scope := range scopes {
|
||||||
switch scope {
|
switch scope {
|
||||||
case "2fa_exempt":
|
case "2fa_exempt":
|
||||||
return coredata.MFAStatusDisabled
|
exempt = true
|
||||||
case "2fa_required":
|
case "2fa_required":
|
||||||
return coredata.MFAStatusEnabled
|
required = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case required && !exempt:
|
||||||
|
return coredata.MFAStatusEnabled
|
||||||
|
case exempt && !required:
|
||||||
|
return coredata.MFAStatusDisabled
|
||||||
|
default:
|
||||||
return coredata.MFAStatusUnknown
|
return coredata.MFAStatusUnknown
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -33,30 +33,22 @@ func TestSendGridDriver(t *testing.T) {
|
|||||||
|
|
||||||
records, err := driver.ListAccounts(context.Background())
|
records, err := driver.ListAccounts(context.Background())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Len(t, records, 3)
|
require.Len(t, records, 1)
|
||||||
|
|
||||||
|
// Recorded against a live SendGrid account that has only the owner. The
|
||||||
|
// list endpoint carries no scopes, so the driver fetches the teammate
|
||||||
|
// detail to read them.
|
||||||
owner := records[0]
|
owner := records[0]
|
||||||
assert.Equal(t, "owner@example.com", owner.Email)
|
assert.Equal(t, "owner@example.com", owner.Email)
|
||||||
assert.Equal(t, "Olivia Owner", owner.FullName)
|
assert.Empty(t, owner.FullName)
|
||||||
assert.Equal(t, "Owner", owner.Role)
|
assert.Equal(t, "Owner", owner.Role)
|
||||||
assert.True(t, owner.IsAdmin)
|
assert.True(t, owner.IsAdmin)
|
||||||
assert.Equal(t, "owner-user", owner.ExternalID)
|
assert.Equal(t, "owner@example.com", owner.ExternalID)
|
||||||
assert.Equal(t, coredata.AccessEntryAccountTypeUser, owner.AccountType)
|
assert.Equal(t, coredata.AccessEntryAccountTypeUser, owner.AccountType)
|
||||||
assert.Equal(t, coredata.MFAStatusEnabled, owner.MFAStatus)
|
// The owner is a full-access user whose scope catalog contains BOTH
|
||||||
|
// 2fa_exempt and 2fa_required, so the MFA signal is ambiguous and the
|
||||||
admin := records[1]
|
// driver reports Unknown rather than guessing from scope ordering.
|
||||||
assert.Equal(t, "admin@example.com", admin.Email)
|
assert.Equal(t, coredata.MFAStatusUnknown, owner.MFAStatus)
|
||||||
assert.Equal(t, "Admin", admin.Role)
|
|
||||||
assert.True(t, admin.IsAdmin)
|
|
||||||
assert.Equal(t, "admin-user", admin.ExternalID)
|
|
||||||
assert.Equal(t, coredata.MFAStatusEnabled, admin.MFAStatus)
|
|
||||||
|
|
||||||
teammate := records[2]
|
|
||||||
assert.Equal(t, "teammate@example.com", teammate.Email)
|
|
||||||
assert.Equal(t, "Teammate", teammate.Role)
|
|
||||||
assert.False(t, teammate.IsAdmin)
|
|
||||||
assert.Equal(t, "teammate-user", teammate.ExternalID)
|
|
||||||
assert.Equal(t, coredata.MFAStatusDisabled, teammate.MFAStatus)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSendGridRole(t *testing.T) {
|
func TestSendGridRole(t *testing.T) {
|
||||||
@@ -127,7 +119,8 @@ func TestSendGridMFAStatus(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{name: "required", scopes: []string{"mail.send", "2fa_required"}, want: coredata.MFAStatusEnabled},
|
{name: "required", scopes: []string{"mail.send", "2fa_required"}, want: coredata.MFAStatusEnabled},
|
||||||
{name: "exempt", scopes: []string{"mail.send", "2fa_exempt"}, want: coredata.MFAStatusDisabled},
|
{name: "exempt", scopes: []string{"mail.send", "2fa_exempt"}, want: coredata.MFAStatusDisabled},
|
||||||
{name: "unknown", scopes: []string{"mail.send"}, want: coredata.MFAStatusUnknown},
|
{name: "both is ambiguous", scopes: []string{"2fa_exempt", "2fa_required", "mail.send"}, want: coredata.MFAStatusUnknown},
|
||||||
|
{name: "neither", scopes: []string{"mail.send"}, want: coredata.MFAStatusUnknown},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
|
|||||||
163
pkg/accessreview/drivers/testdata/sendgrid.yaml
vendored
163
pkg/accessreview/drivers/testdata/sendgrid.yaml
vendored
File diff suppressed because one or more lines are too long
@@ -32,5 +32,8 @@ func sendgridRegistration() *Registration {
|
|||||||
NewDriver: func(_ context.Context, c *http.Client, _ *coredata.Connector, _ *log.Logger) (drivers.Driver, error) {
|
NewDriver: func(_ context.Context, c *http.Client, _ *coredata.Connector, _ *log.Logger) (drivers.Driver, error) {
|
||||||
return drivers.NewSendGridDriver(c), nil
|
return drivers.NewSendGridDriver(c), nil
|
||||||
},
|
},
|
||||||
|
NewNameResolver: func(_ context.Context, c *http.Client, _ *coredata.Connector, _ *log.Logger) drivers.NameResolver {
|
||||||
|
return drivers.NewSendGridNameResolver(c)
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user