Files
svc-api-x/v1/Application.cfc
T
2024-10-23 11:17:42 +03:00

294 lines
14 KiB
Plaintext

<cfcomponent extends="taffy.core.api">
<!---<cfset this.name = "deck_api" />--->
<cfset this.mappings["/resources"] = expandPath("./resources")/>
<cfset this.mappings["/taffy"] = expandPath("./taffy")/>
<cfset this.mappings["/lib"] = expandPath("./lib")/>
<cflock scope="application" type="exclusive" timeout="3"><!---*** не уверен, что это необходимо--->
<cftry>
<!---production environment--->
<cfinclude template="../../conf/prod.cfm"/>
<cfcatch type="ANY">
<cftry>
<!---staging environment--->
<cfinclude template="../../conf/stage.cfm"/>
<cfcatch type="ANY">
<cftry>
<!--- development environment --->
<cfinclude template="../../conf/dev.cfm"/>
<cfcatch type="ANY">
<!---configure right here--->
<!---для ясности--->
<cfset this.config.environment = "dev-default" />
<cfset this.config.datasource = "cmdb" />
<cfset this.config.is_dev_default = "true" /><!---это виднее в сообщении об ошибке--->
<cfset this.config.auth_domain_suffix = "" /><!---эта строка - например, @nubes.ru - вырезается из имени пользователя, полученного с токеном. Case Insensitive--->
<!---https://kk-dev.nubes.ru/auth/realms/mms/protocol/openid-connect/certs--->
<cfset this.config.IDP_certificate = {
"keys": [
{
"kid": "daZqOfZpc_vz7RLAVDWux6jshjD2lFAr5Z2ZNn1dwLA",
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"n": "lQpd1QPTcKr84DLwhW6fOg3KlzvOQUzmrW6AlNFM0Iuiv2RZ5dbHtVaIZMQKTpVy1FbDU70p33rxHgeC_I_7VqzixpX4m2oJrIYkz-hr7hEw93iLRju9XYr5V82jkIL1xLvVASFk7nlnJcMBnGoWXv6j8jhJBD8M5EWNT2hWs6HTpXczYdFCwaVLsGvrUTxnY2iESKEGbahEOfG4LicD7POnMIp-vuEKG-G8aIC7K290uEU8QKSCzKQAQ2viptdO275KAkZUs0r_DDK2msI5SM7JBOTQxAbXyVizsi6idx9yVOoaXCfCb4xYeTg9DVy-oJ2Ukyvs570mEdag6hyURQ",
"e": "AQAB",
"x5c": [
"MIIClTCCAX0CBgF9IxLgpDANBgkqhkiG9w0BAQsFADAOMQwwCgYDVQQDDANtbXMwHhcNMjExMTE1MTAwNzU5WhcNMzExMTE1MTAwOTM5WjAOMQwwCgYDVQQDDANtbXMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCVCl3VA9NwqvzgMvCFbp86DcqXO85BTOatboCU0UzQi6K/ZFnl1se1VohkxApOlXLUVsNTvSnfevEeB4L8j/tWrOLGlfibagmshiTP6GvuETD3eItGO71divlXzaOQgvXEu9UBIWTueWclwwGcahZe/qPyOEkEPwzkRY1PaFazodOldzNh0ULBpUuwa+tRPGdjaIRIoQZtqEQ58bguJwPs86cwin6+4Qob4bxogLsrb3S4RTxApILMpABDa+Km107bvkoCRlSzSv8MMraawjlIzskE5NDEBtfJWLOyLqJ3H3JU6hpcJ8JvjFh5OD0NXL6gnZSTK+znvSYR1qDqHJRFAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAFIVMTudYLL/zyPqlZlU5iQz50KTaZGRH9Pl989TR3ECVOGyCuFGSNu0jWw9qIQME/GHYIZhP06XjCnfJNPeQbyZLet+xDw1BcPY4H0otSCTKXqiFL7ToTyrBWSH7/yzco0yWQ1iN4xPZYpltKOYVmNwBc3bSqcJZ7A3apGs6YjtEkkyTGRtRDFypbsnLYcTa/PWKDzei76cXuM8z7+j8TjIiiQRuxsfobAa0zL9DPzIFSuC6EhVOLVep9clafIxpxMMOT2ktA3cpPxkSC1FaAG5msvigHeXVp+KhWZsjHDyj0HloVnOPEOR8CmY/kyO4iHfalN01nTAx3jHda4o/KA="
],
"x5t": "1l71ENtrLzy7ff8vekF7F-9zRgk",
"x5t##S256": "Mupmx2VbSPMyU9VH_MPFKHm7fAMZouJzHjTiixqT91s"
}
]
}
/>
</cfcatch>
</cftry>
</cfcatch>
</cftry>
</cfcatch>
</cftry>
<cfset this.datasource = this.config.datasource />
<cfset request.config = this.config />
</cflock>
<cfscript>
this.name = hash(getCurrentTemplatePath());
//variables.framework = structNew(); //
variables.framework.debugKey = "debug";
variables.framework.reloadKey = "reload";
variables.framework.reloadPassword = "true";
variables.framework.reloadOnEveryRequest = true;
variables.framework.serializer = "taffy.core.nativeJsonSerializer";
//variables.framework.serializer = "JsonUtilSerializer";
variables.framework.dashboardKey = "dashboard";
variables.framework.disableDashboard = false;
variables.framework.unhandledPaths = "/flex2gateway";
variables.framework.allowCrossDomain = true; // соответствующие заголовки, включая Access-Control-Allow-Origin выставляются только при наличии заголовка origin в реквесте
variables.framework.globalHeaders = structNew();
variables.framework.globalHeaders["Access-Control-Expose-Headers"] = "Location";
//variables.framework.globalHeaders["Access-Control-Allow-Origin"] = "*"; //*** не знаю, нужно ли - но просили этот заголовок
//variables.framework.globalHeaders["X-MY-HEADER"] = "my header value for example";
//variables.framework.docs={};
variables.framework.docs.APIName="MMS API";
variables.framework.docs.APIVersion="3.26";
request.UNDEFINED_USR_ID=-1;
request.ANONYMOUS_USR_ID=2;
request.GUEST_USR_ID=3;
////////////////////////////////////////////////////////
request.ORCHESTRATOR_AUTH="Basic c3ZjX2xrdGVzdDoxMTgzZDIyNzc1OWExYjFkZmVjM2M3NTFiMzIyNzdiYTQ3"; //*** времянка, дубль из корневого Application.cfm
</cfscript>
<cffunction name="onApplicationStart">
<!--- <cfset application.dbUser = "username" />
<cfset application.dbPass = "password" />--->
<!---<cfset application._taffy.settings.allowCrossDomain = true/>--->
<cfreturn super.onApplicationStart() />
</cffunction>
<cffunction name="onRequest">
<cfargument name="template" type="string" required="true"/>
<cfset request.startTickCount=getTickCount()/>
<!---<cfheader name="Access-Control-Allow-Origin" value="*"/>--->
<cfreturn super.onRequest(template) />
</cffunction>
<cfscript>
function onTaffyRequest(verb, cfc, requestArguments, mimeExt, headers){
/* https://docs.taffy.io/#/3.5.0 */
/*
//allow white-listed requests through
if (cfc == "login"){
return true;
}
//otherwise require a device token
if (!structKeyExists(requestArguments, "deviceToken")){
return newRepresentation().noData().withStatus(401, "Authentication Required");
//and make sure it's valid
}else if (!validateToken(requestArguments.deviceToken)){
return newRepresentation().noData().withStatus(403, "Not Authorized");
}
//return representationOf(requestArguments);
//if a token is included, and valid, allow the request to continue
return true;
*/
////////////////////////////////////////////////
// самым грубым образом получаем данные от IDP, не обрабатывая исключения
if (UCase(arguments.verb) EQ 'OPTIONS') return true;
var auth="";
if (structKeyExists(headers,"Authorization")) {
auth=headers.Authorization;
} else {
return representationOf("Authorization header expected").withStatus(401);
}
var httpService = new http(method = "GET", charset = "utf-8", url = "https://personal-cabinet.adl.nubes.ru/api/v1/user", timeout="3");
httpService.addParam(type = "HEADER", name = "Accept", value = "application/json");
httpService.addParam(type = "HEADER", name = "Authorization", value = "#auth#"); //passthrough
var resp = httpService.send();
var result = resp.getPrefix().filecontent;
//writeDump(result); abort;
var idpUserData=deserializeJson(result);
arguments.requestArguments.companyUid=idpUserData.companyId;
arguments.requestArguments.usrUid=idpUserData.contactId;//userId; //GUID!
var usrCustomerInfo=getUsrCustomerInfo(idpUserData.contactId);
if (structIsEmpty(usrCustomerInfo)) return representationOf("Cannot find default specification for current user #result#").withStatus(422);
arguments.requestArguments.usrId=usrCustomerInfo.usrId; //Integer!
arguments.requestArguments.specificationId=usrCustomerInfo.specificationId; //Integer
return true;
////////////////////////////////////////////////////////////////////////////
//ensure https because of basic authentication
if (!lcase(request.config.environment) == "development"){
if (!cgi.server_port_secure) { return newRepresentation().noData().withStatus("403.4", "SSL Required");}
}
request.usr_id=request.UNDEFINED_USR_ID;
if (checkAuth()) {return true;}
//return representationOf(arguments.headers);
//return representationOf(arguments.cfc);
var authToken="";
if (structKeyExists(arguments.headers,"x-auth-token")) {authToken=structFind(arguments.headers,"x-auth-token");}
var issuedFor="";
if (structKeyExists(arguments.headers,"x-issued-for")) {issuedFor=structFind(arguments.headers,"x-issued-for");}
if (validatePin(authToken,issuedFor, arguments.verb, arguments.cfc)) return true; //side effect: sets request.usr_id!
if (!validateCredentials(getBasicAuthCredentials())) {
return newRepresentation().noData().withStatus("401", "Invalid Credentials").withHeaders({"WWW-Authenticate":"Basic realm=mms-api"});
}
return true;
}
//private function validateToken(token){ return false; }
//refactoring required
private function validateCredentials(credentials) {//return true;
if (arguments.credentials.username == "medved" /*AND credentials.password == "ghtdtlrhjcfdxtu!Y"*/) {
request.usr_id=-3;
return true;
}
return false;
}
</cfscript>
<!--- стоит ли сообщать отдельно об экспирации пина
нужно блокировать перебор, можно делать пару pin-issuedFor
вероятно, лучше сделать токен девайса
--->
<cffunction name="validatePin" access="private">
<cfargument name="authToken"/>
<cfargument name="issuedFor"/>
<cfargument name="verb"/>
<cfargument name="cfc"/> <!---для всех запросов, кроме GET,OPTIONS проверяем разрешенные CFC--->
<!---catch db issue--->
<cfquery name="qCheckToken">
select creator_id
from auth_token
where auth_token=<cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.authToken#" />
AND issued_for=<cfqueryparam cfsqltype="cf_sql_varchar" value="#arguments.issuedFor#" />
AND dt_valid_till > <cfqueryparam cfsqltype="cf_sql_timestamp" value="#Now()#" />
</cfquery>
<!---тут можно добавить защиту от перебора с блокировкой попыток (можно добавить поле-счетчик ошибок)--->
<!--- <cfdump var=#qCheckToken#/>--->
<cfif qCheckToken.recordCount GT 0>
<cfset request.usr_id=qCheckToken.creator_id /> <!---impersonate pin issuer--->
<cfif uCase(arguments.verb) EQ "GET" OR uCase(arguments.verb) EQ "OPTIONS">
<cfreturn true/>
<cfelseif listFind("work_order,maint,maint_ls,maint_op,maint_op_ls,work_order_engineer_signature,work_order_spare_ls,work_order_spare,work_order_doc,maint_start,maint_doc_ls,maint_op_spare,maint_op_spare_ls,maint_doc", lCase(arguments.cfc))>
<cfreturn true/>
</cfif>
</cfif>
<cfreturn false/>
</cffunction>
<cffunction name="checkAuth">
<!---https://www.sjoerdlangkemper.nl/2018/09/12/authorization-header-and-cors/--->
<cfset var jwtHelper=CreateObject("component","lib.jwt").init()/>
<cfset var headers=#GetHttpRequestData().headers#/>
<cftry>
<cfset var rawToken=right(headers.Authorization,len(headers.Authorization)-len('bearer '))/>
<cfset var token=jwtHelper.decode(token=rawToken, key=request.config.IDP_certificate.keys[1], algorithms='RS256')/>
<cfset var login=token.preferred_username/>
<!---отрезаем первичный домен --->
<cfset login=ReplaceNoCase(login,"#request.config.auth_domain_suffix#","")/>
<cfquery name="local.qUsr">
select usr_id from usr where login=<cfqueryparam cfsqltype="cf_sql_varchar" value="#login#"/>
</cfquery>
<cfif #local.qUsr.recordCount# EQ 0>
<cfheader statuscode="401" statustext="User not found in local database"/>
<cfreturn false/>
</cfif>
<cfset request.usr_id=local.qUsr.usr_id/>
<cfcatch type="database">
<cfheader statuscode="500" statustext="Internal Server Error - Database Error"/>
<cfoutput>#cfcatch.message# : #cfcatch.detail#</cfoutput>
<cfreturn false/>
</cfcatch>
<cfcatch type="ANY">
<cfif !GetHttpRequestData().method EQ "OPTIONS">
<cfoutput>#cfcatch.message# : #cfcatch.detail#</cfoutput>
<cfreturn false/>
</cfif>
</cfcatch>
</cftry>
<cfreturn true/>
</cffunction>
<cffunction name="getUsrCustomerInfo">
<cfargument name="usrUid"/>
<!--- глупейшее определение спецификации по умолчанию (надо переделать, чтобы создавалась, или вообще просто записывать факты без спеки). Но помним, что при постановке на тестирование цены еше не фиксированы --->
<!--- заметим, что у нас контрагент может быть определен параллельно, от IDP --->
<cfset local={}/>
<!--- Во избежании потери времени при отладке временно сделаем создание дефолтового контракта и спецификации, если их нет --->
<cfquery name="local.qGetCustomerInfo">
select u.usr_id, s.specification_id
from usr u
join contract c on (u.contragent_id=c.contragent_id)
join specification s on (c.contract_id=s.contract_id)
where idp_usr_uid=<cfqueryparam cfsqltype="cf_sql_other" value="#arguments.usrUid#" null=#!isValid('guid',arguments.usrUid)#/>
order by specification_id desc limit 1;
</cfquery>
<!--- <cfdump var=#local.qGetCustomerInfo#/><cfabort/> --->
<cfif local.qGetCustomerInfo.recordCount GT 0>
<cfreturn {"usrId"=#local.qGetCustomerInfo.usr_id#, "specificationId"=#local.qGetCustomerInfo.specification_id#}/>
<cfelse>
<cfreturn {}/> <!--- неуклюжий способ обозначить ошибку --->
</cfif>
</cffunction>
</cfcomponent>