
Security News
npm Adopts OIDC for Trusted Publishing in CI/CD Workflows
npm now supports Trusted Publishing with OIDC, enabling secure package publishing directly from CI/CD workflows without relying on long-lived tokens.
@sourceloop/core
Advanced tools
npm install @sourceloop/core
@sourceloop/core
is the application core for the sourceloop
. It contains
that are used throughout the service catalog.
Adapters help objects with different interfaces collaborate. Here’s how it works:
export class AnyAdapter implements Adapter<any, any> {
adaptToModel(resp: any): any {
return resp;
}
adaptFromModel(data: any): any {
return data;
}
}
export interface Adapter<T, R> {
adaptToModel(resp: R, ...rest: any[]): T;
adaptFromModel(data: T, ...rest: any[]): R;
}
The interface contains parameters(optional) accepted by that command implementing this interface and an execute function.It's used to create different commands like save,update or delete that accepts some parameters and execute function using these parameters
export interface ICommand {
parameters?: any;
execute(): Promise<any>;
}
export class SaveCommand implements ICommand {
public parameters: SaveStateParameters;
execute(): Promise<any> {
//This is intentional.
}
}
Components serves like a vehicle to group extension contributions as context Bindings and various artifacts to allow easier extensibility in Application. Sourceloop core provides three components i.e.bearer-verifier,logger-extension and swagger-authentication
import {
BearerVerifierBindings,
BearerVerifierComponent,
BearerVerifierConfig,
BearerVerifierType,
} from '@sourceloop/core';
setupSequence() {
this.application.sequence(ServiceSequence);
// Mount authentication component for default sequence
this.application.component(AuthenticationComponent);
// Mount bearer verifier component
this.application.bind(BearerVerifierBindings.Config).to({
authServiceUrl: '',
type: BearerVerifierType.service,
} as BearerVerifierConfig);
this.application.component(BearerVerifierComponent);
}
By default asymmetric verification is supported, but if symmetric verification of the token is needed then it has to provided explicitly in the setupSequence() in the manner given below.
this.application.bind(BearerVerifierBindings.Config).to({
authServiceUrl: '',
type: BearerVerifierType.service,
useSymmetricEncryption: true,
} as BearerVerifierConfig);
Create a new Loopback4 Application (If you don't have one already)
lb4 testapp
.
Configure @sourceloop/core
component to include LoggerExtensionComponent
-
this.bind(LOGGER.BINDINGS.LOG_ACTION.toProvider(
LoggerProvider,
);
Start the application
npm start
Latest Log Format
The latest log format that will be generated is as follows:
[2024-03-05T07:43:16.991Z] info :: gg-444-hh :: App_Log -> [200] Request GET /audit-logs Completed in 37ms`
This format includes the timestamp, log level, context, log key, status code, and the log message.
(log: LogEntry) =>
`[${log.timestamp}] ${log.level} :: ${log.context} :: ${log.key} ->[${log.statusCode}] ${log.message}`;
Logging Usage
For example,To log information using the updated format, the info method can be used.
info(
message: string,
context?: string,
statusCode?: STATUS_CODE,
key?: string,
): void
this.logger.info(
`Request ${context.request.method} ${
context.request.url
} Completed in ${Date.now() - requestTime}ms`,
`${context.request.headers.tenantId}`,
`${context.request.res.statusCode}`,
);
A Loopback Component that adds an authenticating middleware for Rest Explorer
lb4 testapp
npm i @sourceloop/core
@sourceloop/core
component to include SwaggerAuthenticateComponent
-
this.bind(SFCoreBindings.config).to({
authenticateSwaggerUI: true,
swaggerUsername: '<username>',
swaggerPassword: '<password>',
});
HttpAuthenticationVerifier
to override the basic authentication logic provided by default.npm start
OBF is used for application programming interface (API) monitoring checks to see if API-connected resources are available, working properly and responding to calls ,detect and alert on errors, warnings, and failed API authentications to ensure secure data exchange etc. For enabling this we need to provide its configuration as follows:
// To check if monitoring is enabled from env or not
const enableObf = !!+(process.env.ENABLE_OBF ?? 0);
// To check if authorization is enabled for swagger stats or not
const authentication =
process.env.SWAGGER_USER && process.env.SWAGGER_PASSWORD ? true : false;
this.bind(SFCoreBindings.config).to({
enableObf,
obfPath: process.env.OBF_PATH ?? '/obf',
openapiSpec: openapi,
authentication: authentication,
swaggerUsername: process.env.SWAGGER_USER,
swaggerPassword: process.env.SWAGGER_PASSWORD,
});
The above specification will show all the API by default, but if you wish to hide certain APIs on swagger stats you can pass the 'modifyPathDefinition' callback method to the above bindings as shown below. Refer for more details.
import {OASPathDefinition} from '@sourceloop/core';
this.bind(SFCoreBindings.config).to({
enableObf,
obfPath: process.env.OBF_PATH ?? '/obf',
openapiSpec: openapi,
modifyPathDefinition(apiPath: string, pathDefinition: OASPathDefinition) {
if (apiPath.startsWith('/auth')) {
delete pathDefinition.post;
return pathDefinition;
}
return pathDefinition;
},
authentication: authentication,
swaggerUsername: process.env.SWAGGER_USER,
swaggerPassword: process.env.SWAGGER_PASSWORD,
});
TenantContextMiddlewareInterceptorProvider
The TenantContextMiddlewareInterceptorProvider here is a middleware designed to modify the HTTP request by adding tenantId from the current user and sets it as a header in the outgoing request. This middleware is typically used for multitenancy scenarios where the tenant ID needs to be communicated in the HTTP headers.
For enabling this we need to provide its configuration as follows:
this.bind(SFCoreBindings.config).to({
enableObf,
obfPath: process.env.OBF_PATH ?? '/obf',
openapiSpec: openapi,
authentication: authentication,
swaggerUsername: process.env.SWAGGER_USER,
swaggerPassword: process.env.SWAGGER_PASSWORD,
authenticateSwaggerUI: authentication,
tenantContextMiddleware: true,
tenantContextEncryptionKey: 'Secret_Key',
});
This middleware uses TenantIdEncryptionProvider here which is responsible for encrypting tenant ID using CryptoJS library to encrypt the provided tenantId when tenantContextEncryptionKey
is added to to core configuration.If the key exists, it encrypts the tenant ID before adding it to the request headers; otherwise, it adds the ID without encryption.
OBF LOGS
Also,If tenantContextMiddleware
is enabled in the coreConfig, it adds an onResponseFinish callback
to the middlewareConfig that incorporates the addTenantId
function to append tenant ID information to responses.The addTenantId
function adds a tenant ID to the response object based on whether a tenantContextEncryptionKey
is provided for decryption. If no key is provided, it assigns the tenant ID directly from the request header; otherwise, it decrypts the encrypted tenant ID and assigns the decrypted value to the response object.
export function addTenantId(
req: IncomingMessage,
res: ServerResponse<IncomingMessage>,
reqResponse: AnyObject,
secretKey?: string,
) {
if (!secretKey) {
reqResponse['tenant-id'] = req.headers['tenant-id'];
} else {
const encryptedTenantId = req.headers['tenant-id'];
const decryptedTenantId = CryptoJS.AES.decrypt(
encryptedTenantId as string,
secretKey as string,
).toString(CryptoJS.enc.Utf8);
reqResponse['tenant-id'] = decryptedTenantId;
}
}
Open ApiSpecification:The OpenAPI Specification (OAS) defines a standard, language-agnostic interface to RESTful APIs which allows us to discover and understand the capabilities of the service without access to source code, documentation, or through network traffic inspection. When properly defined,user can understand and interact with the remote service with a minimal amount of implementation logic.It is a self-contained or composite resource which defines or describes an API or elements of an API.
constructor(options: ApplicationConfig = {}) {
const port = 3000;
options.rest = options.rest ?? {};
options.rest.basePath = process.env.BASE_PATH ?? '';
options.rest.port = +(process.env.PORT ?? port);
options.rest.host = process.env.HOST;
options.rest.openApiSpec = {
endpointMapping: {
[`${options.rest.basePath}/openapi.json`]: {
version: '3.0.0',
format: 'json',
},
},
};
super(options);
this.api({
openapi: '3.0.0',
info: {
title: 'Service Name',
version: '1.0.0',
},
paths: {},
components: {
securitySchemes: SECURITY_SCHEME_SPEC,
},
servers: [{url: '/'}],
});
Similarly to hide APIs from swagger we have a custom OASEnhancer extension that is binded to the CoreComponent. This extension requires a list of APIs to be passed via a binding to hide the APIs. The binding can be provided like this
import {OASBindings, HttpMethod} from '@sourceloop/core';
this.bind(OASBindings.HiddenEndpoint).to([
{
path: '/auth/facebook',
httpMethod: HttpMethod.POST,
},
{
path: '/auth/facebook-auth-redirect',
httpMethod: HttpMethod.GET,
},
]);
Tenant Guard prevents cross db operations through a Loopback repository.
Configure and load TenantUtilitiesComponent
in the application constructor
as shown below.
import {TenantUtilitiesComponent} from '@sourceloop/core';
// ...
export class MyApplication extends BootMixin(
ServiceMixin(RepositoryMixin(RestApplication)),
) {
constructor(options: ApplicationConfig = {}) {
this.component(TenantUtilitiesComponent);
// ...
}
// ...
}
Add the TenantGuardMixin
mixin to a repository on which you want to prevent cross DB operations and provide and service type following the ITenantGuard
with the name tenantGuardService
-
export class TestModelRepository extends TenantGuardMixin(
DefaultCrudRepository<
TestModel,
typeof TestModelRelations.prototype.id,
TestModelRelations
>,
) {
tenantGuardService: ITenantGuard<TestModel, string | undefined>;
constructor(@inject('datasources.db') dataSource: DbDataSource) {
super(TestModel, dataSource);
}
}
Add the tenantGuard()
decorator to your repository class -
import {tenantGuard} from '@sourceloop/core';
// ...
@tenantGuard()
export class TestWithoutGuardRepo extends DefaultTransactionalRepository<
TestModel,
string,
{}
> {
constructor(@inject('datasources.db') dataSource: juggler.DataSource) {
super(TestModel, dataSource);
}
}
There can be scenariors where you wish to dynamicaly switch your datasources based on certain conditions or configurations. Our DynamicDatasourceComponent
will be of great use in such cases. This component internally uses loopback4-dynamic-datasource. Our component binds this parent component, uses the middleware provider and provides default implementation where we identify the datasource based on the current users tenantId and fetch the datasource config values through AWS SSM.
All you need to do is bind the component in your application.ts file
import {DynamicDatasourceComponent} from '@sourceloop/core';
this.component(DynamicDatasourceComponent);
and provide the list of datasource binding key for each datasource needed in your application.
import {DynamicDataSourceBinding} from '@sourceloop/core';
this.bind(DynamicDataSourceBinding.CONFIG).to({dataSourceNames: ['authDb']});
This key will be one that we use in repository class through datasources.authDb
Since our implementaion depends on current user we have added a sequnce step to invoke the middleware after authentication and before invoke so if you wish to do same follow this
await this.invokeMiddleware(context, {
chain: MiddlewareChain.PRE_INVOKE,
});
The TenantGuardMixin
uses a service of the type ITenantGuard
to perform the modification of requests on a tenant, If you want to modify the modification logic, you can bind any service following this interface in your repo to a property with name tenantGuardService
.
The decorator uses a binding on key TenantUtilitiesBindings.GuardService
with the type ITenantGuard
. It uses a default implementation provided in (TenantGuardService
)[/src/components/tenant-utilities/services/tenant-guard.service.ts], to override this, implement a class from scratch or extending this class, and the binding that class to the TenantUtilitiesBindings.GuardService
in your application.ts
-
this.bind(TenantUtilitiesBindings.GuardService).toClass(TenantGuardService);
Decorators provide annotations for class methods and arguments. Decorators use
the form @decorator
where decorator
is the name of the function that will be
called at runtime.
This simple decorator allows you to annotate a Controller
method argument. The
decorator will annotate the method argument with the value of the header
X-Transaction-Id
from the request.
Example
class MyController {
@get('/')
getHandler(@txIdFromHeader() txId: string) {
return `Your transaction id is: ${txId}`;
}
}
Enums helps in defining a set of named constants. Using enums can make it easier to document intent, or create a set of distinct cases for both numeric and string-based enums.
For all the enums provided in here, see packages/core/src/enums
.
You can find documentation for mixins. here
A model describes business domain objects(shape of data) to be persisted in the database, for example, Customer, Address, and Order. It usually defines a list of properties with name, type, and other constraints.We use decorators @model
and @property
to annotate or modify the Class members and Class respectively which helps in manipulating the metadata or even integrate with JSON Schema generation.
export abstract class UserModifiableEntity extends BaseEntity {
@property({
type: 'string',
name: 'created_by',
})
createdBy?: string;
@property({
type: 'string',
name: 'modified_by',
})
modifiedBy?: string;
}
In order to use models provided, in your application:
Extend your model class with name of model's class provided in sourceloop core i.e. BaseEntity(for example) replacing entity that will add two attributes to the model Class i.e. createdBy and modifiedBy of this model.
Other models like SuccessResponse model will add attribute success,UpsertResponse model will add created,updated and failed attributes to the model class extending it.
For all models provided in here, see [here]packages/core/src/models
.
You can find documentation for the providers available .here
A Repository represents a specialized Service Interface that provides strong-typed data access (for example, CRUD) operations of a domain model against the underlying database or service Repositories are adding behavior to Models. Models describe the shape of data, Repositories provide behavior like CRUD operations.
A repository Class which will provide CRUD operations for default user modifications adding modifiedby field along with it for the modified user.
Extend repositories with DefaultUserModifyCrudRepository Class replacing DefaultCrudRepository. For example,
export class UserRepository extends DefaultUserModifyCrudRepository<
User,
typeof User.prototype.id,
UserRelations
> {
public readonly tenant: BelongsToAccessor<Tenant, typeof User.prototype.id>;
public readonly credentials: HasOneRepositoryFactory<
UserCredentials,
typeof User.prototype.id
>;
public readonly userTenants: HasManyRepositoryFactory<
UserTenant,
typeof User.prototype.id
>;
constructor(
@inject(`datasources.${UserTenantDataSourceName}`)
dataSource: juggler.DataSource,
@inject.getter(AuthenticationBindings.CURRENT_USER)
protected readonly getCurrentUser: Getter<
IAuthUserWithPermissions | undefined
>,
@repository.getter('TenantRepository')
protected tenantRepositoryGetter: Getter<TenantRepository>,
@repository.getter('UserCredentialsRepository')
protected userCredentialsRepositoryGetter: Getter<UserCredentialsRepository>,
@repository.getter('UserTenantRepository')
protected userTenantRepositoryGetter: Getter<UserTenantRepository>,
@inject('models.User')
private readonly user: typeof Entity & {prototype: User},
)
Similar to DefaultUserModifyCrudRepository for projects using sequelize as the underlying ORM using @loopback/sequelize extension.
In order to use this follow the below import syntax:
import {SequelizeUserModifyCrudRepository} from '@sourceloop/core/sequelize';
and make sure you've @loopback/sequelize installed.
Sourceloop Core has sequences that can be used in application providing casbin-secure sequence,secure-sequence and service sequence.
-Casbin-secure sequence :It uses casbin library to define permissions at level of entity or resource associated with an API call. Casbin authorisation implementation can be performed in two ways:
this.bind(AuthorizationBindings.CONFIG).to({
allowAlwaysPaths: ['/explorer'],
});
this.component(AuthorizationComponent);
this.bind(AuthorizationBindings.CASBIN_ENFORCER_CONFIG_GETTER).toProvider(
CasbinEnforcerConfigProvider,
);
this.bind(AuthorizationBindings.CASBIN_RESOURCE_MODIFIER_FN).toProvider(
CasbinResValModifierProvider,
);
import {Getter, inject, Provider} from '@loopback/context';
import {HttpErrors} from '@loopback/rest';
import {
AuthorizationBindings,
AuthorizationMetadata,
CasbinResourceModifierFn,
} from 'loopback4-authorization';
export class CasbinResValModifierProvider
implements Provider<CasbinResourceModifierFn>
{
constructor(
@inject.getter(AuthorizationBindings.METADATA)
private readonly getCasbinMetadata: Getter<AuthorizationMetadata>,
@inject(AuthorizationBindings.PATHS_TO_ALLOW_ALWAYS)
private readonly allowAlwaysPath: string[],
) {}
value(): CasbinResourceModifierFn {
return (pathParams: string[], req: Request) => this.action(pathParams, req);
}
async action(pathParams: string[], req: Request): Promise<string> {
const metadata: AuthorizationMetadata = await this.getCasbinMetadata();
if (
!metadata &&
!!this.allowAlwaysPath.find(path => req.path.indexOf(path) === 0)
) {
return '';
}
if (!metadata) {
throw new HttpErrors.InternalServerError(`Metadata object not found`);
}
const res = metadata.resource;
// Now modify the resource parameter using on path params, as per business logic.
// Returning resource value as such for default case.
return `${res}`;
}
}
import {Provider} from '@loopback/context';
import {
CasbinConfig,
CasbinEnforcerConfigGetterFn,
IAuthUserWithPermissions,
} from 'loopback4-authorization';
import * as path from 'path';
export class CasbinEnforcerConfigProvider
implements Provider<CasbinEnforcerConfigGetterFn>
{
constructor() {}
value(): CasbinEnforcerConfigGetterFn {
return (
authUser: IAuthUserWithPermissions,
resource: string,
isCasbinPolicy?: boolean,
) => this.action(authUser, resource, isCasbinPolicy);
}
async action(
authUser: IAuthUserWithPermissions,
resource: string,
isCasbinPolicy?: boolean,
): Promise<CasbinConfig> {
const model = path.resolve(__dirname, './../../fixtures/casbin/model.conf'); // Model initialization from file path
/**
* import * as casbin from 'casbin';
*
* To initialize model from code, use
* let m = new casbin.Model();
* m.addDef('r', 'r', 'sub, obj, act'); and so on...
*
* To initialize model from string, use
* const text = `
* [request_definition]
* r = sub, obj, act
*
* [policy_definition]
* p = sub, obj, act
*
* [policy_effect]
* e = some(where (p.eft == allow))
*
* [matchers]
* m = r.sub == p.sub && r.obj == p.obj && r.act == p.act
* `;
* const model = casbin.newModelFromString(text);
*/
// Write business logic to find out the allowed resource-permission sets for this user. Below is a dummy value.
//const allowedRes = [{resource: 'session', permission: "CreateMeetingSession"}];
const policy = path.resolve(
__dirname,
'./../../fixtures/casbin/policy.csv',
);
const result: CasbinConfig = {
model,
//allowedRes,
policy,
};
return result;
}
}
import {inject} from '@loopback/context';
import {
FindRoute,
HttpErrors,
InvokeMethod,
ParseParams,
Reject,
RequestContext,
RestBindings,
Send,
SequenceHandler,
} from '@loopback/rest';
import {AuthenticateFn, AuthenticationBindings} from 'loopback4-authentication';
import {
AuthorizationBindings,
AuthorizeErrorKeys,
AuthorizeFn,
UserPermissionsFn,
} from 'loopback4-authorization';
import {AuthClient} from './models/auth-client.model';
import {User} from './models/user.model';
const SequenceActions = RestBindings.SequenceActions;
export class MySequence implements SequenceHandler {
constructor(
@inject(SequenceActions.FIND_ROUTE) protected findRoute: FindRoute,
@inject(SequenceActions.PARSE_PARAMS) protected parseParams: ParseParams,
@inject(SequenceActions.INVOKE_METHOD) protected invoke: InvokeMethod,
@inject(SequenceActions.SEND) public send: Send,
@inject(SequenceActions.REJECT) public reject: Reject,
@inject(AuthenticationBindings.USER_AUTH_ACTION)
protected authenticateRequest: AuthenticateFn<AuthUser>,
@inject(AuthenticationBindings.CLIENT_AUTH_ACTION)
protected authenticateRequestClient: AuthenticateFn<AuthClient>,
@inject(AuthorizationBindings.CASBIN_AUTHORIZE_ACTION)
protected checkAuthorisation: CasbinAuthorizeFn,
@inject(AuthorizationBindings.CASBIN_RESOURCE_MODIFIER_FN)
protected casbinResModifierFn: CasbinResourceModifierFn,
) {}
async handle(context: RequestContext) {
const requestTime = Date.now();
try {
const {request, response} = context;
const route = this.findRoute(request);
const args = await this.parseParams(request, route);
request.body = args[args.length - 1];
await this.authenticateRequestClient(request);
const authUser: User = await this.authenticateRequest(request);
// Invoke Resource value modifier
const resVal = await this.casbinResModifierFn(args);
// Check authorisation
const isAccessAllowed: boolean = await this.checkAuthorisation(
authUser,
resVal,
request,
);
// Checking access to route here
if (!isAccessAllowed) {
throw new HttpErrors.Forbidden(AuthorizeErrorKeys.NotAllowedAccess);
}
const result = await this.invoke(route, args);
this.send(response, result);
} catch (err) {
this.reject(context, err);
}
}
}
@authorize({permissions: ['CreateRole'], resource:'role', isCasbinPolicy: true})
@post(rolesPath, {
responses: {
[STATUS_CODE.OK]: {
description: 'Role model instance',
content: {
[CONTENT_TYPE.JSON]: {schema: {'x-ts-type': Role}},
},
},
},
})
async create(@requestBody() role: Role): Promise<Role> {
return await this.roleRepository.create(role);
}
Secure-sequence :It is an action-based sequence. This sequence is a generated class that contains logger,authenticate and authorize actions in the handle method along with helmet and ratelimiting actions that can be used in facades
Helmet action will get triggered for each request and helps in securing HTTP headers which sets them up to prevent attacks like Cross-Site-Scripting(XSS) etc
Ratelimiting action will trigger ratelimiter middleware for all the requests passing through, providing different rate limiting options at API method level. For example, If you want to keep hard rate limit for unauthorized API requests and want to keep it softer for other API requests.
Service-sequence :It is an action-based sequence. This sequence is a generated class that contains authenticate and authorize actions in the handle method.It is used while working with internal secure API's where user authentication and authorization actions are required.
import {ServiceSequence} from '@sourceloop/core';
if (!this.config?.useCustomSequence) {
this.setupSequence();
}
as follows:
setupSequence() {
if (
!this.config.controller?.authenticate ||
!this.config.controller.authorizations
) {
throw new HttpErrors.InternalServerError(Errors.AUTHENTICATION_SETUP);
}
//providing ServiceSequence here.
this.application.sequence(ServiceSequence);
// Mount authentication component for default sequence
this.application.component(AuthenticationComponent);
// Mount bearer verifier component
this.application.bind(BearerVerifierBindings.Config).to({
authServiceUrl: '',
type: BearerVerifierType.service,
} as BearerVerifierConfig);
this.application.component(BearerVerifierComponent);
// Mount authorization component for default sequence
this.application.bind(AuthorizationBindings.CONFIG).to({
allowAlwaysPaths: ['/explorer'],
});
this.application.component(AuthorizationComponent);
}
}
Read about the ProxyBuilderComponent
here.
FAQs
Sourceloop core package
The npm package @sourceloop/core receives a total of 4,948 weekly downloads. As such, @sourceloop/core popularity was classified as popular.
We found that @sourceloop/core demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 8 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Security News
npm now supports Trusted Publishing with OIDC, enabling secure package publishing directly from CI/CD workflows without relying on long-lived tokens.
Research
/Security News
A RubyGems malware campaign used 60 malicious packages posing as automation tools to steal credentials from social media and marketing tool users.
Security News
The CNA Scorecard ranks CVE issuers by data completeness, revealing major gaps in patch info and software identifiers across thousands of vulnerabilities.