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
3 changes: 3 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[submodule "ory_infra/hydra"]
path = ory_infra/hydra
url = https://github.com/ory/hydra.git
4 changes: 4 additions & 0 deletions config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ services:
admin_endpoint:
access_token: ORY_ACCESS_TOKEN
schema_id: ALETHEIA_SCHEMA_ID
hydra:
url: ORY_SDK_URL
# When using the cloud, the endpoint should be "admin".
admin_endpoint:
feature_flag:
url: GITLAB_FEATURE_FLAG_URL
appName: ENV
Expand Down
3 changes: 3 additions & 0 deletions deployment/config/config-file/modules/main.pkl
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ hidden var = new {
}
ory = (oryConfig) {
admin_endpoint = "admin"
hydra = new {
admin_endpoint = "admin"
}
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions deployment/config/config-file/modules/ory.pkl
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,7 @@ admin_url = read("env:ORY_SDK_URL")
admin_endpoint: String
access_token = read("env:ORY_ACCESS_TOKEN")
schema_id = read("env:ALETHEIA_SCHEMA_ID")
hydra = new {
url: read("env:ORY_SDK_URL")
admin_endpoint: String
}
59 changes: 58 additions & 1 deletion docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
version: "3"
services:
sqlite:
image: busybox
volumes:
- hydra-sqlite:/mnt/sqlite
command: "chmod -R 777 /mnt/sqlite"
mongodb:
container_name: mongodb
image: mongo:6.0.17
Expand All @@ -9,7 +14,6 @@ services:
- ${DATA_PATH}/mongodb/db:/data/db:delegated
ports:
- "${MONGODB_PORT:-27017}:27017"

localstack:
container_name: aletheia-localstack
image: localstack/localstack:3.7.2
Expand Down Expand Up @@ -81,7 +85,60 @@ services:
- "4437:4437"
networks:
- intranet
hydra:
image: oryd/hydra:v2.3.0
build:
context: .
dockerfile: .docker/Dockerfile-local-build
ports:
- "4444:4444" # Public port
- "4445:4445" # Admin port
- "5555:5555" # Port for hydra token user
command: serve -c /etc/config/hydra/hydra.yml all --dev
volumes:
- hydra-sqlite:/mnt/sqlite:rw
- type: bind
source: ./ory_config
target: /etc/config/hydra
pull_policy: missing
environment:
- DSN=sqlite:///mnt/sqlite/db.sqlite?_fk=true&mode=rwc
restart: unless-stopped
depends_on:
- hydra-migrate
- sqlite
networks:
- intranet
hydra-migrate:
image: oryd/hydra:v2.3.0
build:
context: .
dockerfile: .docker/Dockerfile-local-build
environment:
- DSN=sqlite:///mnt/sqlite/db.sqlite?_fk=true&mode=rwc
command: migrate -c /etc/config/hydra/hydra.yml sql up -e --yes
pull_policy: missing
volumes:
- hydra-sqlite:/mnt/sqlite:rw
- type: bind
source: ./ory_config
target: /etc/config/hydra
restart: on-failure
networks:
- intranet
depends_on:
- sqlite
consent:
environment:
- HYDRA_ADMIN_URL=http://hydra:4445
image: oryd/hydra-login-consent-node:v2.3.0
ports:
- "3000:3000"
restart: unless-stopped
networks:
- intranet
networks:
intranet:
volumes:
kratos-sqlite:
hydra-sqlite:
33 changes: 33 additions & 0 deletions ory_config/hydra.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
serve:
cookies:
same_site_mode: Lax

urls:
self:
issuer: http://127.0.0.1:4444
consent: http://127.0.0.1:3000/consent
login: http://127.0.0.1:3000/login
logout: http://127.0.0.1:3000/logout

secrets:
system:
- youReallyNeedToChangeThis

oidc:
subject_identifiers:
supported_types:
- pairwise
- public
pairwise:
salt: youReallyNeedToChangeThis

strategies:
access_token: opaque

ttl:
access_token: 1h
refresh_token: 720h

oauth2:
client_credentials:
default_grant_allowed_scope: true
1 change: 1 addition & 0 deletions ory_infra/hydra
Submodule hydra added at a7579b
6 changes: 5 additions & 1 deletion server/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ import { ChatbotModule } from "./chat-bot/chat-bot.module";
import { VerificationRequestModule } from "./verification-request/verification-request.module";
import { FeatureFlagModule } from "./feature-flag/feature-flag.module";
import { GroupModule } from "./group/group.module";
import { SessionOrM2MGuard } from "./auth/m2m-or-session.guard";
import { M2MGuard } from "./auth/m2m.guard";

@Module({})
export class AppModule implements NestModule {
Expand Down Expand Up @@ -158,14 +160,16 @@ export class AppModule implements NestModule {
},
{
provide: APP_GUARD,
useExisting: SessionGuard,
useExisting: SessionOrM2MGuard,
},
{
provide: APP_GUARD,
useExisting: NameSpaceGuard,
},
NameSpaceGuard,
SessionOrM2MGuard,
SessionGuard,
M2MGuard,
],
};
}
Expand Down
25 changes: 11 additions & 14 deletions server/auth/ability/abilities.guard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,18 @@ import {
Injectable,
UnauthorizedException,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { Reflector } from "@nestjs/core";
import { Configuration, FrontendApi } from "@ory/client";
import { CHECK_ABILITY, RequiredRule } from "./ability.decorator";
import { AbilityFactory } from "./ability.factory";
import { NameSpaceEnum } from "../../auth/name-space/schemas/name-space.schema";
import { User } from "../../entities/user.entity";
import { M2M } from "../../entities/m2m.entity";

@Injectable()
export class AbilitiesGuard implements CanActivate {
constructor(
private reflector: Reflector,
private caslAbilityFactor: AbilityFactory,
private configService: ConfigService
private caslAbilityFactor: AbilityFactory
) {}

async canActivate(context: ExecutionContext): Promise<boolean> {
Expand All @@ -28,20 +27,17 @@ export class AbilitiesGuard implements CanActivate {
) || [];

const request = context.switchToHttp().getRequest();
const oryConfig = new Configuration({
basePath: this.configService.get<string>("ory.url"),
accessToken: this.configService.get<string>("access_token"),
});
const ory = new FrontendApi(oryConfig);
const { data: session } = await ory.toSession({
cookie: request.header("Cookie"),
});
const user = session.identity.traits;
const subject: User | M2M | undefined = request.user;
if (!subject) {
// Not authenticated
return false;
}
const nameSpaceSlug = request.params.namespace || NameSpaceEnum.Main;
const ability = this.caslAbilityFactor.defineAbility(
user,
subject,
nameSpaceSlug
);

try {
rules.forEach((rule) =>
ForbiddenError.from(ability).throwUnlessCan(
Expand All @@ -55,6 +51,7 @@ export class AbilitiesGuard implements CanActivate {
if (error instanceof ForbiddenError) {
throw new UnauthorizedException(error.message);
}
throw error;
}
}
}
8 changes: 7 additions & 1 deletion server/auth/ability/ability.decorator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { SetMetadata } from "@nestjs/common";
import { User } from "../../enities/user.entity";
import { User } from "../../entities/user.entity";
import { Action, Subjects } from "./ability.factory";
import { M2M } from "../../entities/m2m.entity";

export interface RequiredRule {
action: Action;
Expand All @@ -26,3 +27,8 @@ export class AdminUserAbility implements RequiredRule {
action = Action.Manage;
subject = User;
}

export class IntegrationAbility implements RequiredRule {
action = Action.Create;
subject = M2M;
}
43 changes: 25 additions & 18 deletions server/auth/ability/ability.factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
ExtractSubjectType,
InferSubjects,
} from "@casl/ability";
import { User } from "../../enities/user.entity";
import { User } from "../../entities/user.entity";
import { M2M } from "../../entities/m2m.entity";

export enum Action {
Manage = "manage",
Expand All @@ -22,40 +23,46 @@ export enum Roles {
Admin = "admin", //manage
SuperAdmin = "super-admin", //Manage / Not editable
Reviewer = "reviewer", // //read, create, update
Integration = "integration",
}

export enum Status {
Inactive = "inactive",
Active = "active",
}
export type Subjects = InferSubjects<typeof User> | "all";

export type Subjects = InferSubjects<typeof User | typeof M2M> | "all";

export type AppAbility = Ability<[Action, Subjects]>;

@Injectable()
export class AbilityFactory {
defineAbility(user: User, nameSpace: string) {
defineAbility(subject: User | M2M, nameSpace: string) {
const { can, cannot, build } = new AbilityBuilder(
Ability as AbilityClass<AppAbility>
);

if (
user.role[nameSpace] === Roles.Admin ||
user.role[nameSpace] === Roles.SuperAdmin
) {
can(Action.Manage, "all");
} else if (
user.role[nameSpace] === Roles.FactChecker ||
user.role[nameSpace] === Roles.Reviewer
) {
can(Action.Read, "all");
can(Action.Update, "all");
if (subject.isM2M && subject.role[nameSpace] === Roles.Integration) {
can(Action.Create, "all");
} else {
can(Action.Read, "all");
cannot(Action.Create, "all").because(
"special message: only admins!"
);
if (
subject.role[nameSpace] === Roles.Admin ||
subject.role[nameSpace] === Roles.SuperAdmin
) {
can(Action.Manage, "all");
} else if (
subject.role[nameSpace] === Roles.FactChecker ||
subject.role[nameSpace] === Roles.Reviewer
) {
can(Action.Read, "all");
can(Action.Update, "all");
can(Action.Create, "all");
} else {
can(Action.Read, "all");
cannot(Action.Create, "all").because(
"special message: only admins!"
);
}
}

return build({
Expand Down
61 changes: 61 additions & 0 deletions server/auth/base.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
import { Reflector } from "@nestjs/core";
import { ConfigService } from "@nestjs/config";
import { Logger } from "@nestjs/common";
import { Configuration } from "@ory/client";

@Injectable()
export abstract class BaseGuard implements CanActivate {
protected logger = new Logger(BaseGuard.name);
protected oryConfig = new Configuration({
basePath: this.configService.get<string>("ory.url"),
accessToken: this.configService.get<string>("ory.access_token"),
});

constructor(
protected configService: ConfigService,
protected readonly reflector: Reflector
) {}

// Implement this in child guards
abstract canActivate(context: ExecutionContext): Promise<boolean> | boolean;

/**
* Utility to extract a bearer token from Authorization header.
*/
protected extractBearerToken(authHeader?: string): string | null {
if (!authHeader) return null;
const matches = authHeader.match(/Bearer\s+(\S+)/);
return matches?.[1] || null;
}

/**
* You can implement a shared redirect or fallback logic here.
*/
protected checkAndRedirect(request, response, isPublic, redirectPath) {
const isAllowedPublicUrl = [
"/login",
"/unauthorized",
"/_next",
"/api/.ory",
"/api/health",
"/sign-up",
"/api/user/register",
"/api/claim", // Allow this route to be public temporarily for testing
].some((route) => request.url.startsWith(route));

const overridePublicRoutes =
!isAllowedPublicUrl &&
this.configService.get<string>("override_public_routes");

if (
(isPublic && !overridePublicRoutes) ||
request.url.startsWith("/api")
) {
return true;
} else {
response.redirect(redirectPath);
return false;
}
}
}
Loading
Loading