Skip to content

Commit

Permalink
Merge pull request #4609 from alkem-io/conflict
Browse files Browse the repository at this point in the history
[v0.93.0] Roles API, Unauthenticated Explore page (Merge conflict fix)
  • Loading branch information
hero101 authored Oct 11, 2024
2 parents 3d800d8 + 81d5ba0 commit 567be78
Show file tree
Hide file tree
Showing 186 changed files with 8,598 additions and 3,840 deletions.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "alkemio-server",
"version": "0.92.3",
"version": "0.93.0",
"description": "Alkemio server, responsible for managing the shared Alkemio platform",
"author": "Alkemio Foundation",
"private": false,
Expand Down
2 changes: 1 addition & 1 deletion quickstart-services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -280,7 +280,7 @@ services:
whiteboard-collaboration:
container_name: alkemio_dev_whiteboard_collaboration
hostname: whiteboard-collaboration
image: alkemio/whiteboard-collaboration-service:v0.3.1
image: alkemio/whiteboard-collaboration-service:v0.4.0
platform: linux/x86_64
depends_on:
- rabbitmq
Expand Down
6 changes: 3 additions & 3 deletions src/common/enums/alkemio.error.status.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ export enum AlkemioErrorStatus {
PAGINATION_INPUT_OUT_OF_BOUND = 'PAGINATION_INPUT_OUT_OF_BOUND',
PAGINATION_NOT_FOUND = 'PAGINATION_NOT_FOUND',
PAGINATION_PARAM_NOT_FOUND = 'PAGINATION_PARAM_NOT_FOUND',
COMMUNITY_POLICY_ROLE_LIMITS_VIOLATED = 'COMMUNITY_POLICY_ROLE_LIMITS_VIOLATED',
COMMUNITY_MEMBERSHIP = 'COMMUNITY_MEMBERSHIP',
COMMUNITY_INVITATION = 'COMMUNITY_INVITATION',
ROLE_SET_POLICY_ROLE_LIMITS_VIOLATED = 'COMMUNITY_POLICY_ROLE_LIMITS_VIOLATED',
ROLE_SET_MEMBERSHIP = 'COMMUNITY_MEMBERSHIP',
ROLE_SET_INVITATION = 'COMMUNITY_INVITATION',
LOGIN_FLOW_INIT = 'LOGIN_FLOW_INIT',
LOGIN_FLOW = 'LOGIN_FLOW',
SESSION_EXTEND = 'SESSION_EXTEND',
Expand Down
12 changes: 12 additions & 0 deletions src/common/enums/authentication.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { registerEnumType } from '@nestjs/graphql';

export enum AuthenticationType {
LINKEDIN = 'linkedin',
MICROSOFT = 'microsoft',
EMAIL = 'email',
UNKNOWN = 'unknown',
}

registerEnumType(AuthenticationType, {
name: 'AuthenticationType',
});
1 change: 1 addition & 0 deletions src/common/enums/authorization.policy.type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export enum AuthorizationPolicyType {
ROOM = 'room',
AI_PERSONA = 'ai-persona',
APPLICATION = 'application',
ROLE_SET = 'role-set',
COMMUNITY = 'community',
COMMUNITY_GUIDELINES = 'community-guidelines',
INVITATION = 'invitation',
Expand Down
6 changes: 3 additions & 3 deletions src/common/enums/community.role.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { registerEnumType } from '@nestjs/graphql';

export enum CommunityRole {
export enum CommunityRoleType {
MEMBER = 'member',
LEAD = 'lead',
ADMIN = 'admin',
}

registerEnumType(CommunityRole, {
name: 'CommunityRole',
registerEnumType(CommunityRoleType, {
name: 'CommunityRoleType',
});
1 change: 1 addition & 0 deletions src/common/enums/logging.context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,4 +71,5 @@ export enum LogContext {
AI_PERSONA_SERVICE_ENGINE = 'ai-persona-service-engine',
AI_SERVER_EVENT_BUS = 'ai-server-event-bus',
SUBSCRIPTION_PUBLISH = 'subscription-publish',
KRATOS = 'kratos',
}
2 changes: 1 addition & 1 deletion src/common/exceptions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ export * from './relationship.not.found.exception';
export * from './validation.exception';
export * from './registration.exception';
export * from './matrix.entity.not.found.exception';
export * from './community.policy.role.limits.exception';
export * from './role.set.policy.role.limits.exception';
export * from './subscription';

export * from './pagination';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { LogContext, AlkemioErrorStatus } from '@common/enums';
import { BaseException } from './base.exception';

export class CommunityInvitationException extends BaseException {
export class RoleSetInvitationException extends BaseException {
constructor(error: string, context: LogContext, code?: AlkemioErrorStatus) {
super(error, context, code ?? AlkemioErrorStatus.COMMUNITY_INVITATION);
super(error, context, code ?? AlkemioErrorStatus.ROLE_SET_INVITATION);
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { LogContext, AlkemioErrorStatus } from '@common/enums';
import { BaseException } from './base.exception';

export class CommunityMembershipException extends BaseException {
export class RoleSetMembershipException extends BaseException {
constructor(error: string, context: LogContext, code?: AlkemioErrorStatus) {
super(error, context, code ?? AlkemioErrorStatus.COMMUNITY_MEMBERSHIP);
super(error, context, code ?? AlkemioErrorStatus.ROLE_SET_MEMBERSHIP);
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { LogContext, AlkemioErrorStatus } from '@common/enums';
import { BaseException } from './base.exception';

export class CommunityPolicyRoleLimitsException extends BaseException {
export class RoleSetPolicyRoleLimitsException extends BaseException {
constructor(error: string, context: LogContext, code?: AlkemioErrorStatus) {
super(
error,
context,
code ?? AlkemioErrorStatus.COMMUNITY_POLICY_ROLE_LIMITS_VIOLATED
code ?? AlkemioErrorStatus.ROLE_SET_POLICY_ROLE_LIMITS_VIOLATED
);
}
}
1 change: 1 addition & 0 deletions src/common/utils/stringify.util.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export function stringifyWithoutAuthorizationMetaInfo(object: any): string {
return JSON.stringify(object, (key, value) => {
if (
key === 'credentials' ||
key === 'authorization' ||
key === 'createdDate' ||
key === 'updatedDate' ||
Expand Down
1 change: 0 additions & 1 deletion src/core/authentication.agent.info/agent.info.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { ICredential } from '@domain/agent/credential';
import { IVerifiedCredential } from '@domain/agent/verified-credential/verified.credential.interface';

export class AgentInfo {
userID = '';
email = '';
Expand Down
175 changes: 131 additions & 44 deletions src/core/authentication/authentication.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@ import { LogContext } from '@common/enums';
import { WINSTON_MODULE_NEST_PROVIDER } from 'nest-winston';
import { UserService } from '@domain/community/user/user.service';
import { AgentInfo } from '../authentication.agent.info/agent.info';
import { OryDefaultIdentitySchema } from './ory.default.identity.schema';
import {
OryDefaultIdentitySchema,
OryTraits,
} from './ory.default.identity.schema';
import { NotSupportedException } from '@common/exceptions';
import { AgentService } from '@domain/agent/agent/agent.service';
import { getBearerToken } from './get.bearer.token';
Expand All @@ -20,6 +23,7 @@ import { AgentInfoCacheService } from '@core/authentication.agent.info/agent.inf
import { getSession } from '@common/utils';
import ConfigUtils from '@config/config.utils';
import { AlkemioConfig } from '@src/types';
import { AgentInfoMetadata } from '@core/authentication.agent.info/agent.info.metadata';

@Injectable()
export class AuthenticationService {
Expand Down Expand Up @@ -88,32 +92,96 @@ export class AuthenticationService {
return this.createAgentInfo(oryIdentity);
}

/**
* Creates and returns an `AgentInfo` object based on the provided Ory identity and session.
*
* @param oryIdentity - Optional Ory identity schema containing user traits.
* @param session - Optional session information.
* @returns A promise that resolves to an `AgentInfo` object.
*
* This method performs the following steps:
* 1. Validates the provided Ory identity.
* 2. Checks for cached agent information based on the email from the Ory identity.
* 3. Builds basic agent information if no cached information is found.
* 4. Maps the authentication type from the session.
* 5. Retrieves additional metadata for the agent.
* 6. Populates the agent information with the retrieved metadata.
* 7. Adds verified credentials if enabled.
* 8. Caches the agent information.
*/
async createAgentInfo(
oryIdentity?: OryDefaultIdentitySchema,
session?: Session
): Promise<AgentInfo> {
const agentInfo = new AgentInfo();
if (!oryIdentity) {
return agentInfo;
}
if (!oryIdentity) return new AgentInfo();

const oryTraits = this.validateEmail(oryIdentity);

const cachedAgentInfo = await this.getCachedAgentInfo(oryTraits.email);
if (cachedAgentInfo) return cachedAgentInfo;

const agentInfo = this.buildBasicAgentInfo(oryIdentity, session);

const agentInfoMetadata = await this.getAgentInfoMetadata(agentInfo.email);
if (!agentInfoMetadata) return agentInfo;

this.populateAgentInfoWithMetadata(agentInfo, agentInfoMetadata);
await this.addVerifiedCredentialsIfEnabled(
agentInfo,
agentInfoMetadata.agentID
);

await this.agentCacheService.setAgentInfoCache(agentInfo);
return agentInfo;
}

/**
* Validates the email trait of the provided Ory identity.
*
* @param oryIdentity - The Ory identity schema containing traits to be validated.
* @returns The validated Ory traits.
* @throws NotSupportedException - If the email trait is missing or empty.
*/
private validateEmail(oryIdentity: OryDefaultIdentitySchema): OryTraits {
const oryTraits = oryIdentity.traits;
if (!oryTraits.email || oryTraits.email.length === 0) {
throw new NotSupportedException(
'Session without email encountered',
LogContext.AUTH
);
}
return oryTraits;
}

const cachedAgentInfo = await this.agentCacheService.getAgentInfoFromCache(
oryTraits.email
);
if (cachedAgentInfo) return cachedAgentInfo;
/**
* Retrieves the cached agent information for a given email.
*
* @param email - The email address of the agent.
* @returns A promise that resolves to the agent information if found in the cache, or undefined if not found.
*/
private async getCachedAgentInfo(
email: string
): Promise<AgentInfo | undefined> {
return await this.agentCacheService.getAgentInfoFromCache(email);
}

/**
* Builds and returns an `AgentInfo` object based on the provided Ory identity schema and session.
*
* @param oryIdentity - The Ory identity schema containing user traits and verifiable addresses.
* @param session - Optional session object containing session details like expiration time.
* @returns An `AgentInfo` object populated with the user's email, name, avatar URL, and session expiry.
*/
private buildBasicAgentInfo(
oryIdentity: OryDefaultIdentitySchema,
session?: Session
): AgentInfo {
const agentInfo = new AgentInfo();
const oryTraits = oryIdentity.traits;
const isEmailVerified =
oryIdentity.verifiable_addresses.find(x => x.via === 'email')?.verified ??
false;
// Have a valid identity, get the information from Ory

agentInfo.email = oryTraits.email;
agentInfo.emailVerified = isEmailVerified;
agentInfo.firstName = oryTraits.name.first;
Expand All @@ -122,58 +190,77 @@ export class AuthenticationService {
agentInfo.expiry = session?.expires_at
? new Date(session.expires_at).getTime()
: undefined;
let agentInfoMetadata;

return agentInfo;
}

/**
* Retrieves the agent information metadata for a given email.
*
* @param email - The email address of the user whose agent information metadata is to be retrieved.
* @returns A promise that resolves to the agent information metadata if found, or undefined if the user is not registered.
* @throws Will log an error message if the user is not registered.
*/
private async getAgentInfoMetadata(
email: string
): Promise<AgentInfoMetadata | undefined> {
try {
agentInfoMetadata = await this.userService.getAgentInfoMetadata(
agentInfo.email
);
return await this.userService.getAgentInfoMetadata(email);
} catch (error) {
this.logger.verbose?.(
`User not registered: ${agentInfo.email}, ${error}`,
`User not registered: ${email}, ${error}`,
LogContext.AUTH
);
return undefined;
}
}

if (!agentInfoMetadata) {
this.logger.verbose?.(
`User: no profile: ${agentInfo.email}`,
LogContext.AUTH
);
// No credentials to obtain, pass on what is there
return agentInfo;
}
this.logger.verbose?.(
`Use: registered: ${agentInfo.email}`,
LogContext.AUTH
);
/**
* Populates the given `agentInfo` object with metadata from `agentInfoMetadata`.
*
* @param agentInfo - The agent information object to be populated.
* @param agentInfoMetadata - The metadata containing information to populate the agent info.
*
* @remarks
* This method assigns the `agentID`, `userID`, and `communicationID` from `agentInfoMetadata` to `agentInfo`.
* If `agentInfoMetadata` contains credentials, they are also assigned to `agentInfo`.
* If credentials are not available, a warning is logged.
*/
private populateAgentInfoWithMetadata(
agentInfo: AgentInfo,
agentInfoMetadata: AgentInfoMetadata
): void {
agentInfo.agentID = agentInfoMetadata.agentID;
agentInfo.userID = agentInfoMetadata.userID;
agentInfo.communicationID = agentInfoMetadata.communicationID;

if (!agentInfoMetadata.credentials) {
if (agentInfoMetadata.credentials) {
agentInfo.credentials = agentInfoMetadata.credentials;
} else {
this.logger.warn?.(
`Authentication Info: Unable to retrieve credentials for registered user: ${agentInfo.email}`,
LogContext.AUTH
);
} else {
agentInfo.credentials = agentInfoMetadata.credentials;
}
agentInfo.agentID = agentInfoMetadata.agentID;
agentInfo.userID = agentInfoMetadata.userID;
agentInfo.communicationID = agentInfoMetadata.communicationID;
}

// Store also retrieved verified credentials; todo: likely slow, need to evaluate other options
/**
* Adds verified credentials to the agent information if SSI (Self-Sovereign Identity) is enabled.
*
* @param agentInfo - The information of the agent to which verified credentials will be added.
* @param agentID - The unique identifier of the agent.
* @returns A promise that resolves when the operation is complete.
*/
private async addVerifiedCredentialsIfEnabled(
agentInfo: AgentInfo,
agentID: string
): Promise<void> {
const ssiEnabled = this.configService.get('ssi.enabled', { infer: true });

if (ssiEnabled) {
const VCs = await this.agentService.getVerifiedCredentials(
agentInfoMetadata.agentID
);

agentInfo.verifiedCredentials = VCs;
const verifiedCredentials =
await this.agentService.getVerifiedCredentials(agentID);
agentInfo.verifiedCredentials = verifiedCredentials;
}

await this.agentCacheService.setAgentInfoCache(agentInfo);

return agentInfo;
}

public async extendSession(sessionToBeExtended: Session): Promise<void> {
Expand Down
Loading

0 comments on commit 567be78

Please sign in to comment.