Adapters help objects with different interfaces collaborate. Here’s how it works:
The adapter gets an interface, compatible with one of the existing objects.
Using this interface, the existing object can safely call the adapter’s methods.
Upon receiving a call, the adapter passes the request to the second object, but in a format
and order of that the second object.It can act two-way adapter that can convert the calls in both directions.
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
exportinterfaceICommand{parameters?:any;execute():Promise<any>;}exportclassSaveCommandimplementsICommand{publicparameters: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
setupSequence(){this.application.sequence(ServiceSequence);// Mount authentication component for default sequencethis.application.component(AuthenticationComponent);// Mount bearer verifier componentthis.application.bind(BearerVerifierBindings.Config).to({authServiceUrl:'',type:BearerVerifierType.service,}asBearerVerifierConfig);this.application.component(BearerVerifierComponent);}
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 notconstenableObf=!!+(process.env.ENABLE_OBF??0);// To check if authorization is enabled for swagger stats or notconstauthentication=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.
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.
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
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 -
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-
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.
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.
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.
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.
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.
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:
Using default casbin policy document - Define policy document in default casbin format in the app, and configure authorise decorator to use those policies.
Defining custom logic to form dynamic policies - Implement dynamic permissions based on app logic in casbin-enforcer-config provider. Authorisation extension will dynamically create casbin policy using this business logic to give the authorisation decisions.
Implement the Casbin Resource value modifier provider. Customise the resource value based on business logic using route arguments parameter in the provider.
import{Getter,inject,Provider}from'@loopback/context';import{HttpErrors}from'@loopback/rest';import{AuthorizationBindings,AuthorizationMetadata,CasbinResourceModifierFn,}from'loopback4-authorization';exportclassCasbinResValModifierProviderimplementsProvider<CasbinResourceModifierFn>{constructor(@inject.getter(AuthorizationBindings.METADATA)privatereadonlygetCasbinMetadata:Getter<AuthorizationMetadata>,@inject(AuthorizationBindings.PATHS_TO_ALLOW_ALWAYS)privatereadonlyallowAlwaysPath:string[],){}value():CasbinResourceModifierFn{return(pathParams:string[],req:Request)=>this.action(pathParams,req);}asyncaction(pathParams:string[],req:Request):Promise<string>{constmetadata:AuthorizationMetadata=awaitthis.getCasbinMetadata();if(!metadata&&!!this.allowAlwaysPath.find(path=>req.path.indexOf(path)===0)){return'';}if(!metadata){thrownewHttpErrors.InternalServerError(`Metadata object not found`);}constres=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*aspathfrom'path';exportclassCasbinEnforcerConfigProviderimplementsProvider<CasbinEnforcerConfigGetterFn>{constructor(){}value():CasbinEnforcerConfigGetterFn{return(authUser:IAuthUserWithPermissions,resource:string,isCasbinPolicy?:boolean,)=>this.action(authUser,resource,isCasbinPolicy);}asyncaction(authUser:IAuthUserWithPermissions,resource:string,isCasbinPolicy?:boolean,):Promise<CasbinConfig>{constmodel=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"}];constpolicy=path.resolve(__dirname,'./../../fixtures/casbin/policy.csv',);constresult:CasbinConfig={model,//allowedRes,policy,};returnresult;}}
Further it can be used by adding a step in custom sequence to check for authorization whenever any end point is hit.
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';constSequenceActions=RestBindings.SequenceActions;exportclassMySequenceimplementsSequenceHandler{constructor(@inject(SequenceActions.FIND_ROUTE)protectedfindRoute:FindRoute,@inject(SequenceActions.PARSE_PARAMS)protectedparseParams:ParseParams,@inject(SequenceActions.INVOKE_METHOD)protectedinvoke:InvokeMethod,@inject(SequenceActions.SEND)publicsend:Send,@inject(SequenceActions.REJECT)publicreject:Reject,@inject(AuthenticationBindings.USER_AUTH_ACTION)protectedauthenticateRequest:AuthenticateFn<AuthUser>,@inject(AuthenticationBindings.CLIENT_AUTH_ACTION)protectedauthenticateRequestClient:AuthenticateFn<AuthClient>,@inject(AuthorizationBindings.CASBIN_AUTHORIZE_ACTION)protectedcheckAuthorisation:CasbinAuthorizeFn,@inject(AuthorizationBindings.CASBIN_RESOURCE_MODIFIER_FN)protectedcasbinResModifierFn:CasbinResourceModifierFn,){}asynchandle(context:RequestContext){constrequestTime=Date.now();try{const{request,response}=context;constroute=this.findRoute(request);constargs=awaitthis.parseParams(request,route);request.body=args[args.length-1];awaitthis.authenticateRequestClient(request);constauthUser:User=awaitthis.authenticateRequest(request);// Invoke Resource value modifierconstresVal=awaitthis.casbinResModifierFn(args);// Check authorisationconstisAccessAllowed:boolean=awaitthis.checkAuthorisation(authUser,resVal,request,);// Checking access to route hereif(!isAccessAllowed){thrownewHttpErrors.Forbidden(AuthorizeErrorKeys.NotAllowedAccess);}constresult=awaitthis.invoke(route,args);this.send(response,result);}catch(err){this.reject(context,err);}}}
Now we can add access permission keys to the controller methods using authorize
decorator as below. Set isCasbinPolicy parameter to use casbin default policy format. Default is false.
@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}},},},},})asynccreate(@requestBody()role:Role):Promise<Role>{returnawaitthis.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.
setupSequence(){if(!this.config.controller?.authenticate||!this.config.controller.authorizations){thrownewHttpErrors.InternalServerError(Errors.AUTHENTICATION_SETUP);}//providing ServiceSequence here.this.application.sequence(ServiceSequence);// Mount authentication component for default sequencethis.application.component(AuthenticationComponent);// Mount bearer verifier componentthis.application.bind(BearerVerifierBindings.Config).to({authServiceUrl:'',type:BearerVerifierType.service,}asBearerVerifierConfig);this.application.component(BearerVerifierComponent);// Mount authorization component for default sequencethis.application.bind(AuthorizationBindings.CONFIG).to({allowAlwaysPaths:['/explorer'],});this.application.component(AuthorizationComponent);}}
You can also use custom sequence instead by providing custom sequence name in the application.