-
Notifications
You must be signed in to change notification settings - Fork 914
chore: handle deleted organizations in idp sync #17405
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
Changes from all commits
6046b56
c1cc257
c267137
a9a9cfa
349cec2
47e76f1
764b944
92744cf
6ddd69e
909c895
0dab7bb
3b49f6c
3f9ebcb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -55,8 +55,13 @@ SELECT | |
FROM | ||
organizations | ||
WHERE | ||
-- Optionally include deleted organizations | ||
deleted = @deleted AND | ||
-- Optionally provide a filter for deleted organizations. | ||
CASE WHEN | ||
sqlc.narg('deleted') :: boolean IS NULL THEN | ||
true | ||
ELSE | ||
deleted = sqlc.narg('deleted') | ||
END AND | ||
Comment on lines
+58
to
+64
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. Adds the ability to query for all orgs, regardless of deletion |
||
id = ANY( | ||
SELECT | ||
organization_id | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -92,14 +92,16 @@ func (s AGPLIDPSync) SyncOrganizations(ctx context.Context, tx database.Store, u | |
return nil // No sync configured, nothing to do | ||
} | ||
|
||
expectedOrgs, err := orgSettings.ParseClaims(ctx, tx, params.MergedClaims) | ||
expectedOrgIDs, err := orgSettings.ParseClaims(ctx, tx, params.MergedClaims) | ||
if err != nil { | ||
return xerrors.Errorf("organization claims: %w", err) | ||
} | ||
|
||
// Fetch all organizations, even deleted ones. This is to remove a user | ||
// from any deleted organizations they may be in. | ||
existingOrgs, err := tx.GetOrganizationsByUserID(ctx, database.GetOrganizationsByUserIDParams{ | ||
UserID: user.ID, | ||
Deleted: false, | ||
Deleted: sql.NullBool{}, | ||
}) | ||
if err != nil { | ||
return xerrors.Errorf("failed to get user organizations: %w", err) | ||
|
@@ -109,10 +111,35 @@ func (s AGPLIDPSync) SyncOrganizations(ctx context.Context, tx database.Store, u | |
return org.ID | ||
}) | ||
|
||
// finalExpected is the final set of org ids the user is expected to be in. | ||
// Deleted orgs are omitted from this set. | ||
finalExpected := expectedOrgIDs | ||
if len(expectedOrgIDs) > 0 { | ||
// If you pass in an empty slice to the db arg, you get all orgs. So the slice | ||
// has to be non-empty to get the expected set. Logically it also does not make | ||
// sense to fetch an empty set from the db. | ||
Comment on lines
+118
to
+120
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. yeah but this turns it into our problem. wouldn't this usually be the result of a configuration error? 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. I don't follow. You can configure it such that a user is in 0 orgs. Even if it's a configuration mistake on the admin, it is a valid configuration. |
||
expectedOrganizations, err := tx.GetOrganizations(ctx, database.GetOrganizationsParams{ | ||
IDs: expectedOrgIDs, | ||
// Do not include deleted organizations. Omitting deleted orgs will remove the | ||
// user from any deleted organizations they are a member of. | ||
Deleted: false, | ||
}) | ||
if err != nil { | ||
return xerrors.Errorf("failed to get expected organizations: %w", err) | ||
} | ||
finalExpected = db2sdk.List(expectedOrganizations, func(org database.Organization) uuid.UUID { | ||
return org.ID | ||
}) | ||
} | ||
|
||
// Find the difference in the expected and the existing orgs, and | ||
// correct the set of orgs the user is a member of. | ||
add, remove := slice.SymmetricDifference(existingOrgIDs, expectedOrgs) | ||
notExists := make([]uuid.UUID, 0) | ||
add, remove := slice.SymmetricDifference(existingOrgIDs, finalExpected) | ||
// notExists is purely for debugging. It logs when the settings want | ||
// a user in an organization, but the organization does not exist. | ||
notExists := slice.DifferenceFunc(expectedOrgIDs, finalExpected, func(a, b uuid.UUID) bool { | ||
return a == b | ||
}) | ||
for _, orgID := range add { | ||
_, err := tx.InsertOrganizationMember(ctx, database.InsertOrganizationMemberParams{ | ||
OrganizationID: orgID, | ||
|
@@ -123,9 +150,30 @@ func (s AGPLIDPSync) SyncOrganizations(ctx context.Context, tx database.Store, u | |
}) | ||
if err != nil { | ||
if xerrors.Is(err, sql.ErrNoRows) { | ||
// This should not happen because we check the org existence | ||
// beforehand. | ||
notExists = append(notExists, orgID) | ||
continue | ||
} | ||
|
||
if database.IsUniqueViolation(err, database.UniqueOrganizationMembersPkey) { | ||
// If we hit this error we have a bug. The user already exists in the | ||
// organization, but was not detected to be at the start of this function. | ||
// Instead of failing the function, an error will be logged. This is to not bring | ||
// down the entire syncing behavior from a single failed org. Failing this can | ||
// prevent user logins, so only fatal non-recoverable errors should be returned. | ||
// | ||
// Inserting a user is privilege escalation. So skipping this instead of failing | ||
// leaves the user with fewer permissions. So this is safe from a security | ||
// perspective to continue. | ||
s.Logger.Error(ctx, "syncing user to organization failed as they are already a member, please report this failure to Coder", | ||
slog.F("user_id", user.ID), | ||
slog.F("username", user.Username), | ||
slog.F("organization_id", orgID), | ||
slog.Error(err), | ||
) | ||
continue | ||
} | ||
return xerrors.Errorf("add user to organization: %w", err) | ||
} | ||
} | ||
|
@@ -141,6 +189,7 @@ func (s AGPLIDPSync) SyncOrganizations(ctx context.Context, tx database.Store, u | |
} | ||
|
||
if len(notExists) > 0 { | ||
notExists = slice.Unique(notExists) // Remove duplicates | ||
s.Logger.Debug(ctx, "organizations do not exist but attempted to use in org sync", | ||
slog.F("not_found", notExists), | ||
slog.F("user_id", user.ID), | ||
|
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.
A dbmem bug I found
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.
oh this is nasty, took me a minute to realize what the bug was. 💀
if you want, rather than track
deleted
yourself, you could do something likeThere 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 had thought about that as well. That felt a bit less direct and less intuitive to me though, so I went with the crude and simple lol