Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,20 @@ import (
"context"
"encoding/json"
"log/slog"
"strings"
)

// wso2EmailDomain is WSO2's own corporate domain. The SCIM "external" org can
// never contain such an account -- it's reserved for WSO2 staff -- so a
// wso2.com email skips the lookup even when ServiceNow tags the row with a
// non-"internal" userType/role (e.g. a wso2.com contact recorded under a
// customer-facing role like snc_external for testing).
const wso2EmailDomain = "@wso2.com"

func isWso2Email(email string) bool {
return strings.HasSuffix(strings.ToLower(email), wso2EmailDomain)
}

// externalAccountStatus is the SCIM "external" org lock/existence status
// appended to GET /users/{id} for external contacts, mirroring the
// asgardeo-user-check service's {exists, locked} contract. Locked is null
Expand Down Expand Up @@ -59,7 +71,7 @@ func (h *UsersHandler) withExternalAccountStatus(ctx context.Context, raw []byte
_ = json.Unmarshal(rawType, &identity.UserType)
}

if identity.Email == "" || identity.UserType == "internal" {
if identity.Email == "" || identity.UserType == "internal" || isWso2Email(identity.Email) {
return raw
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,39 @@ func TestGetUser_ExternalAccountStatus_SkippedForInternalStaff(t *testing.T) {
}
}

// TestGetUser_ExternalAccountStatus_SkippedForWso2Email: a ServiceNow row can
// carry a wso2.com email under a customer-facing role/userType (e.g. a
// wso2.com contact recorded under snc_external for testing) -- that account
// can never exist in the SCIM "external" org, so the lookup must not run.
func TestGetUser_ExternalAccountStatus_SkippedForWso2Email(t *testing.T) {
const id = "11111111-1111-1111-1111-111111111111"
called := false
h := NewUsersHandler(&mockSCIMClient{
searchExternalUserFn: func(_ context.Context, _ string) (*scim.ExternalUserInfo, error) {
called = true
return &scim.ExternalUserInfo{Exists: true}, nil
},
}, &mockEntityUserClient{
getUserFn: func(_ context.Context, _ string) ([]byte, error) {
return []byte(`{"id":"` + id + `","email":"tester@wso2.com","userType":"external"}`), nil
},
}, testDirectory(t))

r := withUser(httptest.NewRequest(http.MethodGet, "/users/"+id, nil))
r.SetPathValue("id", id)
w := httptest.NewRecorder()
h.GetUser(w, r)

assertStatus(t, w, http.StatusOK)
if called {
t.Error("SearchExternalUser was called for a wso2.com email, want no SCIM external lookup")
}
got := decodeJSON[map[string]any](t, w)
if _, ok := got["externalAccount"]; ok {
t.Error("externalAccount present for a wso2.com email, want absent")
}
}

// TestGetUser_ExternalAccountStatus_FailureDoesNotFailTheRequest: a SCIM
// error must not turn a 200 into an error response -- this enrichment is
// best-effort, same as teams.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,23 @@ describe("UserProfilePage", () => {
expect(screen.queryByText("External account")).not.toBeInTheDocument();
});

// A wso2.com contact can be tagged with a customer-facing userType/role in
// ServiceNow (e.g. for testing) despite never being able to exist in the
// SCIM "external" org, which is reserved for WSO2 staff. The field/alert
// must stay hidden even when externalAccount data is present.
it("does not render the External account field or locked alert for a wso2.com email, even if externalAccount data is present", () => {
mockQueryResult({
data: {
...BLOCKED_EXTERNAL_USER,
email: "tester@wso2.com",
externalAccount: { exists: true, locked: true },
},
});
renderPage();
expect(screen.queryByText("External account")).not.toBeInTheDocument();
expect(screen.queryByText(/external account is locked/i)).not.toBeInTheDocument();
});

it("falls back to browser history when no origin was captured (e.g. a bookmarked/direct link)", () => {
mockQueryResult({ data: INTERNAL_USER });
// Two history entries (unlike renderPage's single-entry default) so
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,22 @@ function isInternalUser(user: NormalizedUserDetail): boolean {
);
}

const WSO2_EMAIL_DOMAIN = "@wso2.com";

/**
* True for a wso2.com email, regardless of `userType`/`roles`. The SCIM
* "external" org can never contain such an account (reserved for WSO2
* staff), so the External account field/alert are skipped for one even when
* ServiceNow tags the row with a non-internal role -- e.g. a wso2.com
* contact recorded under a customer-facing role for testing. Narrower than
* {@link isInternalUser}: it only gates the SCIM-sourced UI below, not the
* page's broader internal/external framing (team vs. project access, etc.),
* which ServiceNow's own `userType`/roles still own.
*/
function isWso2Email(email: string): boolean {
return email.toLowerCase().endsWith(WSO2_EMAIL_DOMAIN);
}

type ChipColor = "success" | "warning" | "error" | "default";

interface ProjectAccessStatus {
Expand Down Expand Up @@ -322,7 +338,7 @@ function AccessibleProjectsCard({ user }: { user: NormalizedUserDetail }): JSX.E
</Alert>
)}

{user.externalAccount?.locked === true && (
{user.externalAccount?.locked === true && !isWso2Email(user.email) && (
<Alert severity="error" variant="outlined">
This user's external account is locked — they can't sign in until it's unlocked.
</Alert>
Expand Down Expand Up @@ -556,7 +572,9 @@ export default function UserProfilePage(): JSX.Element {
<Typography variant="body2">{user.phone ?? "Not set"}</Typography>
</MetaCell>
)}
{!internal && <ExternalAccountMetaCell status={user.externalAccount} />}
{!internal && !isWso2Email(user.email) && (
<ExternalAccountMetaCell status={user.externalAccount} />
)}
<MetaCell label="Created on">
<Typography variant="body2">{formatDateTime(user.createdOn)}</Typography>
</MetaCell>
Expand Down