@better-auth/scim 1.6.0-beta.0 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client.mjs +1 -1
- package/dist/index.d.mts +2365 -2298
- package/dist/index.mjs +45 -24
- package/dist/{version-BqMHXjG-.mjs → version-Cf5gNNxE.mjs} +1 -1
- package/package.json +6 -6
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { t as PACKAGE_VERSION } from "./version-
|
|
1
|
+
import { t as PACKAGE_VERSION } from "./version-Cf5gNNxE.mjs";
|
|
2
2
|
import { base64Url } from "@better-auth/utils/base64";
|
|
3
3
|
import { APIError, createAuthEndpoint, createAuthMiddleware, sessionMiddleware } from "better-auth/api";
|
|
4
4
|
import { APIError as APIError$1, HIDE_METADATA } from "better-auth";
|
|
@@ -614,7 +614,21 @@ const generateSCIMTokenBodySchema = z.object({
|
|
|
614
614
|
});
|
|
615
615
|
const getSCIMProviderConnectionQuerySchema = z.object({ providerId: z.string() });
|
|
616
616
|
const deleteSCIMProviderConnectionBodySchema = z.object({ providerId: z.string() });
|
|
617
|
-
|
|
617
|
+
function parseMemberRoles(role) {
|
|
618
|
+
return role.split(",").map((entry) => entry.trim()).filter(Boolean);
|
|
619
|
+
}
|
|
620
|
+
function hasRequiredRole(memberRole, requiredRole) {
|
|
621
|
+
return !requiredRole.length || parseMemberRoles(memberRole).some((role) => requiredRole.includes(role));
|
|
622
|
+
}
|
|
623
|
+
function resolveRequiredRoles(ctx, opts) {
|
|
624
|
+
if (opts.requiredRole) return opts.requiredRole;
|
|
625
|
+
const creatorRole = ctx.context.getPlugin("organization")?.options?.creatorRole;
|
|
626
|
+
return Array.from(new Set(["admin", creatorRole ?? "owner"]));
|
|
627
|
+
}
|
|
628
|
+
function isProviderOwnershipEnabled(opts) {
|
|
629
|
+
return opts.providerOwnership?.enabled ?? false;
|
|
630
|
+
}
|
|
631
|
+
async function getSCIMUserOrgMemberships(ctx, userId) {
|
|
618
632
|
const members = await ctx.context.adapter.findMany({
|
|
619
633
|
model: "member",
|
|
620
634
|
where: [{
|
|
@@ -622,7 +636,7 @@ async function getSCIMUserOrgIds(ctx, userId) {
|
|
|
622
636
|
value: userId
|
|
623
637
|
}]
|
|
624
638
|
});
|
|
625
|
-
return new
|
|
639
|
+
return new Map(members.map((member) => [member.organizationId, parseMemberRoles(member.role)]));
|
|
626
640
|
}
|
|
627
641
|
function normalizeSCIMProvider(provider) {
|
|
628
642
|
return {
|
|
@@ -643,13 +657,15 @@ async function findOrganizationMember(ctx, userId, organizationId) {
|
|
|
643
657
|
}]
|
|
644
658
|
});
|
|
645
659
|
}
|
|
646
|
-
async function assertSCIMProviderAccess(ctx, userId, provider) {
|
|
660
|
+
async function assertSCIMProviderAccess(ctx, userId, provider, requiredRole) {
|
|
647
661
|
if (provider.organizationId) {
|
|
648
662
|
if (!ctx.context.hasPlugin("organization")) throw new APIError("FORBIDDEN", { message: "Organization plugin is required to access this SCIM provider" });
|
|
649
|
-
|
|
663
|
+
const member = await findOrganizationMember(ctx, userId, provider.organizationId);
|
|
664
|
+
if (!member) throw new APIError("FORBIDDEN", { message: "You must be a member of the organization to access this provider" });
|
|
665
|
+
if (!hasRequiredRole(member.role, requiredRole)) throw new APIError("FORBIDDEN", { message: "Insufficient role for this operation" });
|
|
650
666
|
} else if (provider.userId && provider.userId !== userId) throw new APIError("FORBIDDEN", { message: "You must be the owner to access this provider" });
|
|
651
667
|
}
|
|
652
|
-
async function checkSCIMProviderAccess(ctx, userId, providerId) {
|
|
668
|
+
async function checkSCIMProviderAccess(ctx, userId, providerId, requiredRole) {
|
|
653
669
|
const provider = await ctx.context.adapter.findOne({
|
|
654
670
|
model: "scimProvider",
|
|
655
671
|
where: [{
|
|
@@ -658,7 +674,7 @@ async function checkSCIMProviderAccess(ctx, userId, providerId) {
|
|
|
658
674
|
}]
|
|
659
675
|
});
|
|
660
676
|
if (!provider) throw new APIError("NOT_FOUND", { message: "SCIM provider not found" });
|
|
661
|
-
await assertSCIMProviderAccess(ctx, userId, provider);
|
|
677
|
+
await assertSCIMProviderAccess(ctx, userId, provider, requiredRole);
|
|
662
678
|
return provider;
|
|
663
679
|
}
|
|
664
680
|
const generateSCIMToken = (opts) => createAuthEndpoint("/scim/generate-token", {
|
|
@@ -682,12 +698,14 @@ const generateSCIMToken = (opts) => createAuthEndpoint("/scim/generate-token", {
|
|
|
682
698
|
}, async (ctx) => {
|
|
683
699
|
const { providerId, organizationId } = ctx.body;
|
|
684
700
|
const user = ctx.context.session.user;
|
|
701
|
+
const requiredRole = resolveRequiredRoles(ctx, opts);
|
|
685
702
|
if (providerId.includes(":")) throw new APIError("BAD_REQUEST", { message: "Provider id contains forbidden characters" });
|
|
686
703
|
if (organizationId && !ctx.context.hasPlugin("organization")) throw new APIError("BAD_REQUEST", { message: "Restricting a token to an organization requires the organization plugin" });
|
|
687
704
|
let member = null;
|
|
688
705
|
if (organizationId) {
|
|
689
706
|
member = await findOrganizationMember(ctx, user.id, organizationId);
|
|
690
707
|
if (!member) throw new APIError("FORBIDDEN", { message: "You are not a member of the organization" });
|
|
708
|
+
if (!hasRequiredRole(member.role, requiredRole)) throw new APIError("FORBIDDEN", { message: "Insufficient role for this operation" });
|
|
691
709
|
}
|
|
692
710
|
const scimProvider = await ctx.context.adapter.findOne({
|
|
693
711
|
model: "scimProvider",
|
|
@@ -700,7 +718,7 @@ const generateSCIMToken = (opts) => createAuthEndpoint("/scim/generate-token", {
|
|
|
700
718
|
}] : []]
|
|
701
719
|
});
|
|
702
720
|
if (scimProvider) {
|
|
703
|
-
await assertSCIMProviderAccess(ctx, user.id, scimProvider);
|
|
721
|
+
await assertSCIMProviderAccess(ctx, user.id, scimProvider, requiredRole);
|
|
704
722
|
await ctx.context.adapter.delete({
|
|
705
723
|
model: "scimProvider",
|
|
706
724
|
where: [{
|
|
@@ -722,7 +740,7 @@ const generateSCIMToken = (opts) => createAuthEndpoint("/scim/generate-token", {
|
|
|
722
740
|
providerId,
|
|
723
741
|
organizationId,
|
|
724
742
|
scimToken: await storeSCIMToken(ctx, opts, baseToken),
|
|
725
|
-
...opts
|
|
743
|
+
...isProviderOwnershipEnabled(opts) ? { userId: user.id } : {}
|
|
726
744
|
}
|
|
727
745
|
});
|
|
728
746
|
if (opts.afterSCIMTokenGenerated) await opts.afterSCIMTokenGenerated({
|
|
@@ -734,13 +752,13 @@ const generateSCIMToken = (opts) => createAuthEndpoint("/scim/generate-token", {
|
|
|
734
752
|
ctx.setStatus(201);
|
|
735
753
|
return ctx.json({ scimToken });
|
|
736
754
|
});
|
|
737
|
-
const listSCIMProviderConnections = () => createAuthEndpoint("/scim/list-provider-connections", {
|
|
755
|
+
const listSCIMProviderConnections = (opts) => createAuthEndpoint("/scim/list-provider-connections", {
|
|
738
756
|
method: "GET",
|
|
739
757
|
use: [sessionMiddleware],
|
|
740
758
|
metadata: { openapi: {
|
|
741
759
|
operationId: "listSCIMProviderConnections",
|
|
742
760
|
summary: "List SCIM providers",
|
|
743
|
-
description: "Returns SCIM providers
|
|
761
|
+
description: "Returns SCIM providers the user owns or has the required org role for.",
|
|
744
762
|
responses: { "200": {
|
|
745
763
|
description: "List of SCIM providers",
|
|
746
764
|
content: { "application/json": { schema: {
|
|
@@ -764,15 +782,18 @@ const listSCIMProviderConnections = () => createAuthEndpoint("/scim/list-provide
|
|
|
764
782
|
} }
|
|
765
783
|
}, async (ctx) => {
|
|
766
784
|
const userId = ctx.context.session.user.id;
|
|
767
|
-
const
|
|
785
|
+
const requiredRole = resolveRequiredRoles(ctx, opts);
|
|
786
|
+
const orgMemberships = ctx.context.hasPlugin("organization") ? await getSCIMUserOrgMemberships(ctx, userId) : /* @__PURE__ */ new Map();
|
|
768
787
|
const providers = (await ctx.context.adapter.findMany({ model: "scimProvider" })).filter((p) => {
|
|
769
|
-
if (p.organizationId)
|
|
770
|
-
|
|
771
|
-
|
|
788
|
+
if (p.organizationId) {
|
|
789
|
+
const roles = orgMemberships.get(p.organizationId);
|
|
790
|
+
return roles ? !requiredRole.length || roles.some((role) => requiredRole.includes(role)) : false;
|
|
791
|
+
}
|
|
792
|
+
return p.userId === userId || !p.userId;
|
|
772
793
|
}).map((p) => normalizeSCIMProvider(p));
|
|
773
794
|
return ctx.json({ providers });
|
|
774
795
|
});
|
|
775
|
-
const getSCIMProviderConnection = () => createAuthEndpoint("/scim/get-provider-connection", {
|
|
796
|
+
const getSCIMProviderConnection = (opts) => createAuthEndpoint("/scim/get-provider-connection", {
|
|
776
797
|
method: "GET",
|
|
777
798
|
use: [sessionMiddleware],
|
|
778
799
|
query: getSCIMProviderConnectionQuerySchema,
|
|
@@ -802,10 +823,10 @@ const getSCIMProviderConnection = () => createAuthEndpoint("/scim/get-provider-c
|
|
|
802
823
|
}, async (ctx) => {
|
|
803
824
|
const { providerId } = ctx.query;
|
|
804
825
|
const userId = ctx.context.session.user.id;
|
|
805
|
-
const provider = await checkSCIMProviderAccess(ctx, userId, providerId);
|
|
826
|
+
const provider = await checkSCIMProviderAccess(ctx, userId, providerId, resolveRequiredRoles(ctx, opts));
|
|
806
827
|
return ctx.json(normalizeSCIMProvider(provider));
|
|
807
828
|
});
|
|
808
|
-
const deleteSCIMProviderConnection = () => createAuthEndpoint("/scim/delete-provider-connection", {
|
|
829
|
+
const deleteSCIMProviderConnection = (opts) => createAuthEndpoint("/scim/delete-provider-connection", {
|
|
809
830
|
method: "POST",
|
|
810
831
|
use: [sessionMiddleware],
|
|
811
832
|
body: deleteSCIMProviderConnectionBodySchema,
|
|
@@ -828,7 +849,7 @@ const deleteSCIMProviderConnection = () => createAuthEndpoint("/scim/delete-prov
|
|
|
828
849
|
}, async (ctx) => {
|
|
829
850
|
const { providerId } = ctx.body;
|
|
830
851
|
const userId = ctx.context.session.user.id;
|
|
831
|
-
await checkSCIMProviderAccess(ctx, userId, providerId);
|
|
852
|
+
await checkSCIMProviderAccess(ctx, userId, providerId, resolveRequiredRoles(ctx, opts));
|
|
832
853
|
await ctx.context.adapter.delete({
|
|
833
854
|
model: "scimProvider",
|
|
834
855
|
where: [{
|
|
@@ -1417,18 +1438,18 @@ const parseSCIMAPIUserFilter = (filter) => {
|
|
|
1417
1438
|
const scim = (options) => {
|
|
1418
1439
|
const opts = {
|
|
1419
1440
|
storeSCIMToken: "plain",
|
|
1420
|
-
providerOwnership: { enabled: false },
|
|
1421
1441
|
...options
|
|
1422
1442
|
};
|
|
1443
|
+
const providerOwnershipEnabled = options?.providerOwnership?.enabled ?? false;
|
|
1423
1444
|
const authMiddleware = authMiddlewareFactory(opts);
|
|
1424
1445
|
return {
|
|
1425
1446
|
id: "scim",
|
|
1426
1447
|
version: PACKAGE_VERSION,
|
|
1427
1448
|
endpoints: {
|
|
1428
1449
|
generateSCIMToken: generateSCIMToken(opts),
|
|
1429
|
-
listSCIMProviderConnections: listSCIMProviderConnections(),
|
|
1430
|
-
getSCIMProviderConnection: getSCIMProviderConnection(),
|
|
1431
|
-
deleteSCIMProviderConnection: deleteSCIMProviderConnection(),
|
|
1450
|
+
listSCIMProviderConnections: listSCIMProviderConnections(opts),
|
|
1451
|
+
getSCIMProviderConnection: getSCIMProviderConnection(opts),
|
|
1452
|
+
deleteSCIMProviderConnection: deleteSCIMProviderConnection(opts),
|
|
1432
1453
|
getSCIMUser: getSCIMUser(authMiddleware),
|
|
1433
1454
|
createSCIMUser: createSCIMUser(authMiddleware),
|
|
1434
1455
|
patchSCIMUser: patchSCIMUser(authMiddleware),
|
|
@@ -1456,7 +1477,7 @@ const scim = (options) => {
|
|
|
1456
1477
|
type: "string",
|
|
1457
1478
|
required: false
|
|
1458
1479
|
},
|
|
1459
|
-
...
|
|
1480
|
+
...providerOwnershipEnabled ? { userId: {
|
|
1460
1481
|
type: "string",
|
|
1461
1482
|
required: false
|
|
1462
1483
|
} } : {}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@better-auth/scim",
|
|
3
|
-
"version": "1.6.0
|
|
3
|
+
"version": "1.6.0",
|
|
4
4
|
"description": "SCIM plugin for Better Auth",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -54,14 +54,14 @@
|
|
|
54
54
|
},
|
|
55
55
|
"devDependencies": {
|
|
56
56
|
"tsdown": "0.21.1",
|
|
57
|
-
"@better-auth/core": "1.6.0
|
|
58
|
-
"@better-auth/sso": "1.6.0
|
|
57
|
+
"@better-auth/core": "1.6.0",
|
|
58
|
+
"@better-auth/sso": "1.6.0"
|
|
59
59
|
},
|
|
60
60
|
"peerDependencies": {
|
|
61
61
|
"@better-auth/utils": "0.4.0",
|
|
62
|
-
"better-call": "
|
|
63
|
-
"@better-auth/core": "^1.6.0
|
|
64
|
-
"better-auth": "^1.6.0
|
|
62
|
+
"better-call": "1.3.5",
|
|
63
|
+
"@better-auth/core": "^1.6.0",
|
|
64
|
+
"better-auth": "^1.6.0"
|
|
65
65
|
},
|
|
66
66
|
"scripts": {
|
|
67
67
|
"build": "tsdown",
|