Skip to content

Commit cfd15fc

Browse files
committed
db/account: refactor checkRequiredSSO and use it to check, if modifying the email_address is allowed or if update_on_login is set, additionally prohibit changing first or last name
1 parent c85f74a commit cfd15fc

File tree

12 files changed

+78
-22
lines changed

12 files changed

+78
-22
lines changed

src/packages/database/postgres-server-queries.coffee

+14
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ read = require('read')
6161
{pii_expire} = require("./postgres/pii")
6262
passwordHash = require("@cocalc/backend/auth/password-hash").default;
6363
registrationTokens = require('./postgres/registration-tokens').default;
64+
getStrategiesSSO = require("@cocalc/database/settings/get-sso-strategies").default;
6465

6566
stripe_name = require('@cocalc/util/stripe/name').default;
6667

@@ -2647,6 +2648,19 @@ exports.extend_PostgreSQL = (ext) -> class PostgreSQL extends ext
26472648
return result.rows[0].organization_id
26482649
return undefined
26492650

2651+
getStrategiesSSO: () =>
2652+
return await getStrategiesSSO()
2653+
2654+
get_email_address_for_account_id: (account_id) =>
2655+
result = await @async_query
2656+
query : 'SELECT email_address FROM accounts'
2657+
where : [ 'account_id :: UUID = $1' ]
2658+
params : [ account_id ]
2659+
if result.rows.length > 0
2660+
result.rows[0].email_address
2661+
else
2662+
return undefined
2663+
26502664
# async
26512665
registrationTokens: (options, query) =>
26522666
return await registrationTokens(@, options, query)

src/packages/database/postgres/types.ts

+3
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
QueryRows,
1515
UntypedQueryResult,
1616
} from "@cocalc/util/types/database";
17+
import type { Strategy } from "@cocalc/util/types/sso";
1718
import { Changes } from "./changefeed";
1819

1920
export type { QueryResult };
@@ -317,6 +318,8 @@ export interface PostgreSQL extends EventEmitter {
317318
email_address: string;
318319
}>;
319320
}): Promise<void>;
321+
322+
getStrategiesSSO(): Promise<Strategy[]>;
320323
}
321324

322325
export interface SetAccountFields {

src/packages/database/settings/get-sso-strategies.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ export default async function getStrategies(): Promise<Strategy[]> {
1919
COALESCE(info -> 'display', conf -> 'display') as display,
2020
COALESCE(info -> 'public', conf -> 'public') as public,
2121
COALESCE(info -> 'exclusive_domains', conf -> 'exclusive_domains') as exclusive_domains,
22-
COALESCE(info -> 'do_not_hide', 'false'::JSONB) as do_not_hide
22+
COALESCE(info -> 'do_not_hide', 'false'::JSONB) as do_not_hide,
23+
COALESCE(info -> 'update_on_login', 'false'::JSONB) as update_on_login
2324
2425
FROM passport_settings
2526
WHERE strategy != 'site_conf'
@@ -39,6 +40,7 @@ export default async function getStrategies(): Promise<Strategy[]> {
3940
public: row.public ?? true,
4041
exclusiveDomains: row.exclusive_domains ?? [],
4142
doNotHide: row.do_not_hide ?? false,
43+
updateOnLogin: row.update_on_login ?? false,
4244
};
4345
});
4446
}

src/packages/next/components/auth/sso.tsx

+2-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { join } from "path";
99
import { CSSProperties, ReactNode, useMemo } from "react";
1010

1111
import { Icon } from "@cocalc/frontend/components/icon";
12-
import { checkRequiredSSO } from "@cocalc/server/auth/sso/check-required-sso";
12+
import { checkRequiredSSO } from "@cocalc/util/auth-check-required-sso";
1313
import { PRIMARY_SSO } from "@cocalc/util/types/passport-types";
1414
import { Strategy } from "@cocalc/util/types/sso";
1515
import Loading from "components/share/loading";
@@ -67,6 +67,7 @@ export default function SSO(props: SSOProps) {
6767
public: true,
6868
exclusiveDomains: [],
6969
doNotHide: true,
70+
updateOnLogin: false,
7071
};
7172

7273
return (

src/packages/server/accounts/set-email-address.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import passwordHash, {
2121
verifyPassword,
2222
} from "@cocalc/backend/auth/password-hash";
2323
import getPool from "@cocalc/database/pool";
24-
import { checkRequiredSSO } from "@cocalc/server/auth/sso/check-required-sso";
24+
import { checkRequiredSSO } from "@cocalc/util/auth-check-required-sso";
2525
import getStrategies from "@cocalc/database/settings/get-sso-strategies";
2626
import {
2727
isValidUUID,

src/packages/server/auth/check-email-exclusive-sso.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55

66
import { PostgreSQL } from "@cocalc/database/postgres/types";
77
import getStrategies from "@cocalc/database/settings/get-sso-strategies";
8-
import { checkRequiredSSO } from "@cocalc/server/auth/sso/check-required-sso";
8+
import { checkRequiredSSO } from "@cocalc/util/auth-check-required-sso";
99

1010
export async function checkEmailExclusiveSSO(
1111
db: PostgreSQL,

src/packages/server/auth/sso/passport-login.ts

+8-6
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@
1313
* via an SSO strategy, we link this passport to your exsiting account. There is just one exception,
1414
* which are SSO strategies which "exclusively" manage a domain.
1515
* 2. If you're not signed in and try to sign in, this checks if there is already an account – and creates it if not.
16-
* 3. If you sign in and the SSO strategy is set to "update_on_login", it will reset the name of the user to the
17-
* data from the SSO provider. However, the user can still modify the name.
16+
* 3. If you sign in and the SSO strategy is set to "update_on_login",
17+
* it will reset the name of the user to the data from the SSO provider.
18+
* Users can only modify their first and last name, if that SSO mechanism isn't exclusive!
1819
* 4. If you already have an email address belonging to a newly introduced exclusive domain, it will start to be controlled by it.
1920
*/
2021

@@ -45,8 +46,9 @@ import { sanitizeProfile } from "@cocalc/server/auth/sso/sanitize-profile";
4546
import { callback2 as cb2 } from "@cocalc/util/async-utils";
4647
import { is_valid_email_address } from "@cocalc/util/misc";
4748
import { HELP_EMAIL } from "@cocalc/util/theme";
48-
import { emailBelongsToDomain, getEmailDomain } from "./check-required-sso";
49+
import { emailBelongsToDomain } from "@cocalc/util/auth-check-required-sso";
4950
import { SSO_API_KEY_COOKIE_NAME } from "./consts";
51+
import { getEmailDomain } from "@cocalc/util/auth-check-required-sso";
5052

5153
const logger = getLogger("server:auth:sso:passport-login");
5254

@@ -240,7 +242,7 @@ export class PassportLogin {
240242
const exclusiveDomains = strategy.info?.exclusive_domains ?? [];
241243
if (!isEmpty(exclusiveDomains)) {
242244
for (const email of opts.emails ?? []) {
243-
const emailDomain = getEmailDomain(email.toLocaleLowerCase());
245+
const emailDomain = getEmailDomain(email.toLowerCase());
244246
for (const ssoDomain of exclusiveDomains) {
245247
if (emailBelongsToDomain(emailDomain, ssoDomain)) {
246248
return true;
@@ -253,7 +255,7 @@ export class PassportLogin {
253255

254256
// similar to the above, for a specific email address
255257
private checkEmailExclusiveSSO(email_address: string): boolean {
256-
const emailDomain = getEmailDomain(email_address.toLocaleLowerCase());
258+
const emailDomain = getEmailDomain(email_address.toLowerCase());
257259
for (const strategyName in this.opts.passports) {
258260
const strategy = this.opts.passports[strategyName];
259261
for (const ssoDomain of strategy.info?.exclusive_domains ?? []) {
@@ -510,7 +512,7 @@ export class PassportLogin {
510512
}
511513

512514
// We update the email address, if it does not belong to another account.
513-
515+
514516
if (is_valid_email_address(locals.email_address)) {
515517
upd.email_address = locals.email_address;
516518
}

src/packages/server/auth/sso/unlink-strategy.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ upstream SSO provider.
1212
import getPool from "@cocalc/database/pool";
1313
import getStrategies from "@cocalc/database/settings/get-sso-strategies";
1414
import { is_valid_uuid_string } from "@cocalc/util/misc";
15-
import { checkRequiredSSO } from "./check-required-sso";
15+
import { checkRequiredSSO } from "@cocalc/util/auth-check-required-sso";
1616

1717
// The name should be something like "google-9999601658192", i.e., a key
1818
// of the passports field.

src/packages/server/auth/throttle.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ the database.
77
import LRU from "lru-cache";
88

99
import getStrategies from "@cocalc/database/settings/get-sso-strategies";
10-
import { checkRequiredSSO } from "./sso/check-required-sso";
10+
import { checkRequiredSSO } from "@cocalc/util/auth-check-required-sso";
1111

1212
const emailShortCache = new LRU<string, number>({
1313
max: 10000, // avoid memory issues

src/packages/server/auth/sso/check-required-sso.ts renamed to src/packages/util/auth-check-required-sso.ts

+7-6
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,19 @@
55

66
import { Strategy } from "@cocalc/util/types/sso";
77

8-
/**
9-
* If the domain of a given email address belongs to an SSO strategy,
10-
* which is configured to be an "exclusive" domain, then return the Strategy.
11-
* This also matches subdomains, i.e. "[email protected]" is goverend by "baz.edu".
12-
*/
13-
148
interface Opts {
159
email: string | undefined;
1610
strategies: Strategy[] | undefined;
1711
specificStrategy?: string;
1812
}
1913

14+
/**
15+
* If the domain of a given email address belongs to an SSO strategy,
16+
* which is configured to be an "exclusive" domain, then return the Strategy.
17+
* This also matches subdomains, i.e. "[email protected]" is goverend by "baz.edu".
18+
*
19+
* Optionally, if @specificStrategy is set, only that strategy is checked!
20+
*/
2021
export function checkRequiredSSO(opts: Opts): Strategy | undefined {
2122
const { email, strategies, specificStrategy } = opts;
2223
// if the domain of email is contained in any of the strategie's exclusiveDomain array, return that strategy's name

src/packages/util/db-schema/accounts.ts

+36-4
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ import {
1515
OTHER_SETTINGS_USERDEFINED_LLM,
1616
} from "./defaults";
1717

18+
import { checkRequiredSSO } from "@cocalc/util/auth-check-required-sso";
1819
import { DEFAULT_LOCALE } from "@cocalc/util/consts/locale";
20+
import { Strategy } from "@cocalc/util/types/sso";
1921

2022
Table({
2123
name: "accounts",
@@ -399,6 +401,7 @@ Table({
399401
// obviously min_balance can't be set!
400402
},
401403
async check_hook(db, obj, account_id, _project_id, cb) {
404+
// db is of type PostgreSQL defined in @cocalc/database/postgres/types
402405
if (obj["name"] != null) {
403406
// NOTE: there is no way to unset/remove a username after one is set...
404407
try {
@@ -415,6 +418,7 @@ Table({
415418
return;
416419
}
417420
}
421+
418422
// Hook to truncate some text fields to at most 254 characters, to avoid
419423
// further trouble down the line.
420424
for (const field of ["first_name", "last_name", "email_address"]) {
@@ -427,10 +431,38 @@ Table({
427431
}
428432
}
429433
}
430-
// check, if account is exclusively controlled by SSO and its update_on_login config is true
431-
const {email_address} = obj
432-
if (email_address != null) {
433-
// TODO
434+
435+
// if account is exclusively controlled by SSO, you're maybe prohibited from changing account details
436+
const current_email_address =
437+
await db.get_email_address_for_account_id(account_id);
438+
console.log({ current_email_address });
439+
if (typeof current_email_address === "string") {
440+
const strategies: Strategy[] = await db.getStrategiesSSO();
441+
const strategy = checkRequiredSSO({
442+
strategies,
443+
email: current_email_address,
444+
});
445+
console.log({ strategy });
446+
console.log(obj);
447+
// we got a required exclusive SSO for the given account_id
448+
if (strategy != null) {
449+
// if user tries to change email_address
450+
if (typeof obj.email_address === "string") {
451+
cb(`You are not allowed to change your email address.`);
452+
return;
453+
}
454+
// ... or tries to change first or last name, but strategy has update_on_login set
455+
if (
456+
strategy.updateOnLogin &&
457+
(typeof obj.first_name === "string" ||
458+
obj.last_name === "string")
459+
) {
460+
cb(
461+
`You are not allowed to change your first or last name. You have to change it at your single-sign-on provider: ${strategy.display}.`,
462+
);
463+
return;
464+
}
465+
}
434466
}
435467
cb();
436468
},

src/packages/util/types/sso.ts

+1
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,5 @@ export interface Strategy {
1212
public: boolean; // true for general broad audiences, like google, default true
1313
exclusiveDomains: string[]; // list of domains, e.g. ["foo.com"], which must go through that SSO mechanism (and block regular email signup)
1414
doNotHide: boolean; // if true and a public=false, show it directly on the login/signup page
15+
updateOnLogin: boolean; // if true and account is goverend by an exclusiveDomain, user's are not allowed to change their first and last name
1516
}

0 commit comments

Comments
 (0)