Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
n1ru4l committed Nov 27, 2024
1 parent 04fe640 commit 5af778b
Show file tree
Hide file tree
Showing 7 changed files with 872 additions and 156 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { type MigrationExecutor } from '../pg-migrator';

export default {
name: '2024.11.21T00-00-00.member-rules-permissions.ts',
run: ({ sql }) => sql`
ALTER TABLE "organization_member_roles"
ALTER "scopes" DROP NOT NULL
, ADD COLUMN "permissions_groups" JSONB
;
CREATE TABLE "organization_member_role_assignments" (
"organization_id" UUID NOT NULL REFERENCES "organizations" ("id") ON DELETE CASCADE
, "user_id" UUID NOT NULL REFERENCES "users" ("id") ON DELETE CASCADE
, "organization_member_role_id" UUID NOT NULL REFERENCES "organization_member_roles" ("id") ON DELETE CASCADE
, "resources" JSONB
PRIMARY KEY ("organization_id", "user_id", "organization_member_role_id")
);
`,
} satisfies MigrationExecutor;
1 change: 1 addition & 0 deletions packages/migrations/src/run-pg-migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,5 +147,6 @@ export const runPGMigrations = async (args: { slonik: DatabasePool; runTo?: stri
await import('./actions/2024.11.12T00-00-00.supertokens-9.1'),
await import('./actions/2024.11.12T00-00-00.supertokens-9.2'),
await import('./actions/2024.11.12T00-00-00.supertokens-9.3'),
await import('./actions/2024.11.21T00-00-00.member-rules-permissions'),
],
});
10 changes: 9 additions & 1 deletion packages/services/api/src/modules/auth/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createModule } from 'graphql-modules';
import { AuthManager } from './providers/auth-manager';
import { OrganizationAccess } from './providers/organization-access';
import { OrganizationMembers } from './providers/organization-members';
import { ProjectAccess } from './providers/project-access';
import { TargetAccess } from './providers/target-access';
import { UserManager } from './providers/user-manager';
Expand All @@ -12,5 +13,12 @@ export const authModule = createModule({
dirname: __dirname,
typeDefs,
resolvers,
providers: [AuthManager, UserManager, OrganizationAccess, ProjectAccess, TargetAccess],
providers: [
AuthManager,
UserManager,
OrganizationAccess,
ProjectAccess,
TargetAccess,
OrganizationMembers,
],
});
2 changes: 1 addition & 1 deletion packages/services/api/src/modules/auth/lib/authz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,7 +369,7 @@ type ActionDefinitionMap = {

type Actions = keyof typeof actionDefinitions;

type ActionStrings = Actions | '*';
export type ActionStrings = Actions | '*';

/** Unauthenticated session that is returned by default. */
class UnauthenticatedSession extends Session {
Expand Down
277 changes: 123 additions & 154 deletions packages/services/api/src/modules/auth/lib/supertokens-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,23 @@ import { isUUID } from '../../../shared/is-uuid';
import { Logger } from '../../shared/providers/logger';
import type { Storage } from '../../shared/providers/storage';
import {
OrganizationAccessScope,
ProjectAccessScope,
TargetAccessScope,
} from '../providers/scopes';
import { AuthNStrategy, AuthorizationPolicyStatement, Session } from './authz';
OrganizationMembers,
OrganizationMembershipRoleAssignment,
} from '../providers/organization-members';
import { ActionStrings, AuthNStrategy, AuthorizationPolicyStatement, Session } from './authz';

export class SuperTokensCookieBasedSession extends Session {
public superTokensUserId: string;
private organizationMembers: OrganizationMembers;
private storage: Storage;

constructor(
args: { superTokensUserId: string; email: string },
deps: { storage: Storage; logger: Logger },
deps: { organizationMembers: OrganizationMembers; storage: Storage; logger: Logger },
) {
super({ logger: deps.logger });
this.superTokensUserId = args.superTokensUserId;
this.organizationMembers = deps.organizationMembers;
this.storage = deps.storage;
}

Expand All @@ -32,17 +33,54 @@ export class SuperTokensCookieBasedSession extends Session {
): Promise<Array<AuthorizationPolicyStatement>> {
const user = await this.getViewer();

this.logger.debug(
'Loading policy statements for organization. (userId=%s, organizationId=%s)',
user.id,
organizationId,
);

if (!isUUID(organizationId)) {
this.logger.debug(
'Invalid organization ID provided. (userId=%s, organizationId=%s)',
user.id,
organizationId,
);

return [];
}

const member = await this.storage.getOrganizationMember({
this.logger.debug(
'Load organization membership for user. (userId=%s, organizationId=%s)',
user.id,
organizationId,
);

const organization = await this.storage.getOrganization({ organizationId });
const organizationMembership = await this.organizationMembers.findOrganizationMembership({
organization,
userId: user.id,
});

console.log(JSON.stringify(organizationMembership, null, 2));

if (!organizationMembership) {
this.logger.debug(
'No membership found, resolve empty policy statements. (userId=%s, organizationId=%s)',
user.id,
organizationId,
);

return [];
}

// owner of organization should have full right to do anything.
if (member?.isOwner) {
if (organizationMembership.isAdmin) {
this.logger.debug(
'User is organization owner, resolve admin access policy. (userId=%s, organizationId=%s)',
user.id,
organizationId,
);

return [
{
action: '*',
Expand All @@ -52,11 +90,76 @@ export class SuperTokensCookieBasedSession extends Session {
];
}

if (Array.isArray(member?.scopes)) {
return transformOrganizationMemberLegacyScopes({ organizationId, scopes: member.scopes });
this.logger.debug(
'Translate organization role assignments to policy statements. (userId=%s, organizationId=%s)',
user.id,
organizationId,
);

const policyStatements = this.translateAssignedRolesToAuthorizationPolicyStatements(
organizationId,
organizationMembership.assignedRoles,
);

return policyStatements;
}

private translateAssignedRolesToAuthorizationPolicyStatements(
organizationId: string,
organizationMembershipRoleAssignments: Array<OrganizationMembershipRoleAssignment>,
): Array<AuthorizationPolicyStatement> {
const policyStatements: Array<AuthorizationPolicyStatement> = [];

for (const assignedRole of organizationMembershipRoleAssignments) {
for (const permissionGroup of assignedRole.role.permissionGroups) {
const resources = assignedRole.resources[permissionGroup.resourceType];

if (!resources) {
// skip is assignment group is missing
continue;
}

const resourceIds: Array<string> = [];

switch (permissionGroup.resourceType) {
case 'organization':
case 'project':
case 'target': {
if (resources === '*') {
resourceIds.push(`hrn:${organizationId}:${permissionGroup.resourceType}/*`);
break;
}
resourceIds.push(
...resources.map(
resourceId => `hrn:${organizationId}:${permissionGroup.resourceType}/${resourceId}`,
),
);
break;
}
case 'service': {
if (resources === '*') {
resourceIds.push(`hrn:${organizationId}:target/*/service/*`);
break;
}
resourceIds.push(
...resources.map(resourceId => {
const [targetId, serviceName] = resourceId.split('/');
return `hrn:${organizationId}:${targetId}/service/${serviceName}`;
}),
);
break;
}
}

policyStatements.push({
action: permissionGroup.permissions as ActionStrings[],
effect: permissionGroup.effect,
resource: resourceIds,
});
}
}

return [];
return policyStatements;
}

public async getViewer(): Promise<User> {
Expand All @@ -78,11 +181,17 @@ export class SuperTokensCookieBasedSession extends Session {

export class SuperTokensUserAuthNStrategy extends AuthNStrategy<SuperTokensCookieBasedSession> {
private logger: ServiceLogger;
private organizationMembers: OrganizationMembers;
private storage: Storage;

constructor(deps: { logger: ServiceLogger; storage: Storage }) {
constructor(deps: {
logger: ServiceLogger;
organizationMembers: OrganizationMembers;
storage: Storage;
}) {
super();
this.logger = deps.logger.child({ module: 'SuperTokensUserAuthNStrategy' });
this.organizationMembers = deps.organizationMembers;
this.storage = deps.storage;
}

Expand Down Expand Up @@ -172,8 +281,9 @@ export class SuperTokensUserAuthNStrategy extends AuthNStrategy<SuperTokensCooki
email: session.email,
},
{
storage: this.storage,
organizationMembers: this.organizationMembers,
logger: args.req.log,
storage: this.storage,
},
);
}
Expand All @@ -184,144 +294,3 @@ const SuperTokenAccessTokenModel = zod.object({
superTokensUserId: zod.string(),
email: zod.string(),
});

function transformOrganizationMemberLegacyScopes(args: {
organizationId: string;
scopes: Array<OrganizationAccessScope | ProjectAccessScope | TargetAccessScope>;
}) {
const policies: Array<AuthorizationPolicyStatement> = [];
for (const scope of args.scopes) {
switch (scope) {
case OrganizationAccessScope.READ: {
policies.push({
effect: 'allow',
action: [
'support:manageTickets',
'project:create',
'project:describe',
'organization:describe',
],
resource: [`hrn:${args.organizationId}:organization/${args.organizationId}`],
});
break;
}
case OrganizationAccessScope.SETTINGS: {
policies.push({
effect: 'allow',
action: [
'organization:modifySlug',
'schemaLinting:modifyOrganizationRules',
'billing:describe',
'billing:update',
],
resource: [`hrn:${args.organizationId}:organization/${args.organizationId}`],
});
break;
}
case OrganizationAccessScope.DELETE: {
policies.push({
effect: 'allow',
action: ['organization:delete'],
resource: [`hrn:${args.organizationId}:organization/${args.organizationId}`],
});
break;
}
case OrganizationAccessScope.INTEGRATIONS: {
policies.push({
effect: 'allow',
action: ['oidc:modify', 'gitHubIntegration:modify', 'slackIntegration:modify'],
resource: [`hrn:${args.organizationId}:organization/${args.organizationId}`],
});
break;
}
case OrganizationAccessScope.MEMBERS: {
policies.push({
effect: 'allow',
action: [
'member:manageInvites',
'member:removeMember',
'member:assignRole',
'member:modifyRole',
'member:describe',
],
resource: [`hrn:${args.organizationId}:organization/${args.organizationId}`],
});
break;
}
case ProjectAccessScope.ALERTS: {
policies.push({
effect: 'allow',
action: ['alert:modify'],
resource: [`hrn:${args.organizationId}:organization/${args.organizationId}`],
});
break;
}
case ProjectAccessScope.READ: {
policies.push({
effect: 'allow',
action: ['project:describe'],
resource: [`hrn:${args.organizationId}:organization/${args.organizationId}`],
});
break;
}
case ProjectAccessScope.DELETE: {
policies.push({
effect: 'allow',
action: ['project:delete'],
resource: [`hrn:${args.organizationId}:organization/${args.organizationId}`],
});
break;
}
case ProjectAccessScope.SETTINGS: {
policies.push({
effect: 'allow',
action: ['project:delete', 'project:modifySettings', 'schemaLinting:modifyProjectRules'],
resource: [`hrn:${args.organizationId}:organization/${args.organizationId}`],
});
break;
}
case TargetAccessScope.READ: {
policies.push({
effect: 'allow',
action: ['appDeployment:describe', 'laboratory:describe', 'target:create'],
resource: [`hrn:${args.organizationId}:organization/${args.organizationId}`],
});
break;
}
case TargetAccessScope.REGISTRY_WRITE: {
policies.push({
effect: 'allow',
action: ['schemaCheck:approve', 'schemaVersion:approve', 'laboratory:modify'],
resource: [`hrn:${args.organizationId}:organization/${args.organizationId}`],
});
break;
}
case TargetAccessScope.TOKENS_WRITE: {
policies.push({
effect: 'allow',
action: ['targetAccessToken:modify', 'cdnAccessToken:modify'],
resource: [`hrn:${args.organizationId}:organization/${args.organizationId}`],
});
break;
}
case TargetAccessScope.SETTINGS: {
policies.push({
effect: 'allow',
action: ['target:modifySettings'],
resource: [`hrn:${args.organizationId}:organization/${args.organizationId}`],
});
break;
}
case TargetAccessScope.DELETE: {
policies.push({
effect: 'allow',
action: ['target:delete'],
resource: [`hrn:${args.organizationId}:organization/${args.organizationId}`],
});
break;
}
}
}

return policies;
}
Loading

0 comments on commit 5af778b

Please sign in to comment.