471 lines
20 KiB
Plaintext
471 lines
20 KiB
Plaintext
<cfcomponent
|
|
displayname="Application"
|
|
output="true"
|
|
hint="Handle the application.">
|
|
|
|
|
|
<!--- Pseudoconstructor --->
|
|
<!--- Set up the application. --->
|
|
<cfset this.Name = "PAYG-REPORT" />
|
|
<cfset this.applicationTimeout = createTimeSpan( 0, 3, 0, 0 ) />
|
|
<cfset this.sessionmanagement="Yes"/>
|
|
<cfset this.clientmanagement="No"/>
|
|
<cfset this.sessiontimeout=CreateTimeSpan(0, 1, 0, 0)/>
|
|
<cfset this.setclientcookies="No"/>
|
|
<cfset this.mappings = structNew() />
|
|
<cfset this.mappings["/mod"] = getDirectoryFromPath(getCurrentTemplatePath()) & "mod/" />
|
|
<cfset this.customTagPaths = expandPath(getDirectoryFromPath(getCurrentTemplatePath()) & "mod")/>
|
|
<cfset this.datasource = "rpt"/>
|
|
<!--- SSO --->
|
|
<cfset this.client_id = "payg-report.deck.nubes.ru"/>
|
|
<cfset this.auth_endpoint = "https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/auth" />
|
|
<cfset this.access_token_endpoint = "https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/token" />
|
|
<cfset this.idpCertUrl = "https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/certs" />
|
|
<cfset this.client_secret = createObject("java", "java.lang.System").getEnv("IDP_CLIENT_SECRET")/>
|
|
<!--- <cfset this.client_secret = this.client_secret ?: ""/> --->
|
|
|
|
<cfset this.defaultdatasource = this.datasource/>
|
|
<cfset request.DS = "#this.datasource#">
|
|
<cfset request.language="">
|
|
<!--- кажется, нужно инициализировать датасорцы в псевдоконструкторе - onRequest не получается --->
|
|
<!--- *** проверить, что есть - если есть, не создавать --->
|
|
<!--- не тут-то было: так не работает --->
|
|
<!--- <cfif NOT structKeyExists(this.datasources,this.datasource)>
|
|
</cfif> --->
|
|
|
|
<!--- это работает с административно заданным датасорцем, хотя, казалось бы, не должно
|
|
(без окружения фунекция вернет пустую структуру). Видимо, административный датасорц обнаруживается раньше.
|
|
Проверять на пустоту не хочется, чтобы не плодить в псевдоконструкторе переменные --->
|
|
<cfset this.datasources["#this.datasource#"]=getDS("#this.datasource#","ds_#this.datasource#")/>
|
|
<!--- <cfdump var=#variables#/> --->
|
|
|
|
|
|
<cffunction
|
|
name="OnApplicationStart"
|
|
access="public"
|
|
returntype="boolean"
|
|
output="false"
|
|
hint="Fires when the application is first created.">
|
|
|
|
<!--- *** мы не проверяем срок жизни сертификата и его отзыв--->
|
|
Obtain idp certificate OnApplicationStart
|
|
<cfset application.idpCertificate = getIdpCertificate(this.idpCertUrl)/>
|
|
|
|
<cfreturn true />
|
|
</cffunction>
|
|
|
|
|
|
<cffunction
|
|
name="OnRequest"
|
|
access="public"
|
|
returntype="void"
|
|
output="true"
|
|
hint="Fires after pre page processing is complete.">
|
|
|
|
<cfargument name="template" type="string" required="true"/>
|
|
|
|
<cfset request.startTickCount=getTickCount()/>
|
|
|
|
<cfset setEncoding("FORM", "UTF-8")>
|
|
<cfset setEncoding("URL", "UTF-8")>
|
|
|
|
<cfset request.PERMISSION_NONE=0/>
|
|
<cfset request.PERMISSION_READ=1/>
|
|
<cfset request.PERMISSION_WRITE=2/>
|
|
|
|
<!--- <cfset request.UNDEFINED_USR_ID=-1/>
|
|
<cfset request.ANONYMOUS_USR_ID=2/>
|
|
<cfset request.GUEST_USR_ID=3/> --->
|
|
|
|
<!--- global settings --->
|
|
<cfset request.RECORDS_PER_PAGE=500/>
|
|
<cfset request.APP_VERSION="0.00.007"/>
|
|
<cfset request.STAND=getStand()/>
|
|
|
|
<!--- application constants --->
|
|
|
|
<!---<cfset request.SMTP_SERVER="172.16.16.16"/>
|
|
<cfset request.MAIL_FROM_FOR_NOTIFICATION="Pipeline tracker <pipeline@mail.ru>"/>
|
|
<cfset request.SUBJECT_PREFIX="[PIPELINE TRACKER] "/>--->
|
|
<!---<cfset request.BASE_URL="https://smtest.dtln.local/pipeline/"/>*** сделать нормально. Внимание! При наличии прокси CGI.SERVER_NAME может быть заменено --->
|
|
|
|
<cfset local = {} />
|
|
|
|
<cfset local.basePath = getDirectoryFromPath(
|
|
getCurrentTemplatePath()
|
|
) />
|
|
|
|
<cfset local.targetPath = getDirectoryFromPath(
|
|
expandPath( arguments.template )
|
|
) />
|
|
|
|
<cfset local.requestDepth = (
|
|
listLen( local.targetPath, "\/" ) -
|
|
listLen( local.basePath, "\/" )
|
|
) />
|
|
|
|
<cfset request.webRoot = repeatString(
|
|
"../",
|
|
local.requestDepth
|
|
) />
|
|
|
|
<!---
|
|
While we wouldn't normally do this for every page
|
|
request (it would normally be cached in the
|
|
application initialization), I'm going to calculate
|
|
the site URL based on the web root.
|
|
--->
|
|
<cfset request.siteUrl = (
|
|
IIF(
|
|
(CGI.server_port_secure <!--- CGI.https EQ "On" does not work with apache+tomcat and nginx+tomcat --->),
|
|
DE( "https://" ),
|
|
DE( "http://" )
|
|
) &
|
|
cgi.http_host &
|
|
reReplace(
|
|
getDirectoryFromPath( arguments.template ), "([^\\/]+[\\/]){#local.requestDepth#}$",
|
|
"",
|
|
"one"
|
|
)
|
|
) />
|
|
|
|
<cfset request.thisPage=Replace(ReplaceNoCase(expandPath(ARGUMENTS.template), local.basePath, ""), "\", "/")/>
|
|
<!--- <cfset request.thisUrl="#request.thisPage##len(CGI.queryString)#"/> чревато циклом --->
|
|
|
|
<!--- получение сертификата может занимать длительное время.
|
|
Блокировать на это время приложение или сессию не следует.
|
|
Вообще не следует ничего блокировать на время сетевого взаимодействия
|
|
Нижеследующая, на первый взгляд, странная конструкция ровно для этого придумана
|
|
--->
|
|
<!--- <cfoutput>onRequest:check for cert started #(getTickCount()-request.startTickCount)#</cfoutput> --->
|
|
<cfset var idpCertificateExists = false/>
|
|
<cflock scope="application" type="readonly" timeout=1>
|
|
<cfset request.APP_NAME=this.Name/>
|
|
<cfset idpCertificateExists = structKeyExists(application,"idpCertificate")/>
|
|
</cflock>
|
|
<cfif NOT idpCertificateExists>
|
|
<!--- Obtain IDP Certificate (onRequest) --->
|
|
<cfset var idpCertificate = getIdpCertificate(this.idpCertUrl) />
|
|
<cflock scope="application" type="exclusive" timeout=1>
|
|
<cfset application.idpCertificate = idpCertificate/>
|
|
</cflock>
|
|
</cfif>
|
|
<!--- <cfoutput>onRequest:check for cert finished #(getTickCount()-request.startTickCount)#</cfoutput> --->
|
|
|
|
|
|
<cfset this.redirect_uri = "https://#CGI.SERVER_NAME#/#CGI.SCRIPT_NAME#" />
|
|
|
|
<cfinclude template="inc/functions.cfm"/>
|
|
|
|
<cfcookie name="CFID" value="#session.CFID#"/>
|
|
<cfcookie name="CFTOKEN" value="#session.CFTOKEN#"/>
|
|
|
|
<!--- SSO --->
|
|
<cflock scope="session" type="exclusive" timeout="3">
|
|
<!---
|
|
<cfdump var=#session#/>
|
|
<cfoutput>#(getTickCount()-request.startTickCount)#</cfoutput>--->
|
|
<!---
|
|
Это первая проба, с сессией
|
|
Когда протухнет сессия, мы можем пойти к IDP и повторно аутентифицироваться. Поскольку у нас сохраняются куки IDP, его сессия, возможно, будет жива. Если мы обновляли сессию IDP в другом приложении SSO, по всей видимости, она будет продолжаться. Если мы видим, что сессия IDP состарилась и хочет протухнуть, мы можем ее освежить и получить новый access token (который мы пока не используем - мы пользуемся ответом IDP) Наверное, правильно обходиться для этих целей вообще без сессии *** --->
|
|
<!---
|
|
проверить sso
|
|
? сделать рефреш
|
|
? сделать логаут
|
|
? сделать отображение ошибки IDP
|
|
+ сделать получение сертификата
|
|
|
|
--->
|
|
<cfif structKeyExists(session, "auth")>
|
|
<cfset request.auth = structCopy(session.auth)/>
|
|
<!--- <cfset request.auth.wz = "WZ01553"/> --->
|
|
<!--- тут нужно сделать проверку и рефреш токена --->
|
|
<!--- сессия есть --->
|
|
<cfelse>
|
|
<!--- сессии нет --->
|
|
<cfset session.auth_state=createGUID().toString()/>
|
|
|
|
<cfscript>
|
|
jwt = new lib.jwt(this.client_secret);
|
|
idp = new lib.oauth2(this.client_id, this.client_secret, this.auth_endpoint, this.access_token_endpoint, this.redirect_uri);
|
|
|
|
/*
|
|
часто появляется ошибка: после неудачного запроса к IDP
|
|
в адресной строке браузера остается все, с чем его редиректил IDP,
|
|
а повторный запрос с этим контентом уже не работает. Получаем 400 Bad Request -
|
|
может быть, KeyCloak так защищается от циклических редиректов
|
|
Не очень понятно, как предотвратить повторный запрос (можно, конечно, использовать сессию, но странно)
|
|
*/
|
|
//writeOutput(getTickCount()-request.startTickCount);
|
|
if(structKeyExists(url, "code")) { //обрабатываем редирект от IDP *** наличие поля code в URL введет нас в заблуждение, но эvar = data, label =то только без валидной сессии
|
|
//dump(var = url.code, label = "URL.code");
|
|
// Request access token from idp with the
|
|
// authorization code that we got via the URL
|
|
//aFormFields = [];
|
|
//data = idp.makeAccessTokenRequest((url.code).content, aFormFields);
|
|
try {
|
|
writeOutput("idp.makeAccessTokenRequest");
|
|
writeOutput(getTickCount()-request.startTickCount);
|
|
resp = idp.makeAccessTokenRequest(url.code/*, aFormFields*/);
|
|
writeOutput("idp.makeAccessTokenRequest Done");
|
|
writeOutput(getTickCount()-request.startTickCount);
|
|
//dump(resp);
|
|
data = deserializeJson(resp.content);
|
|
//writedump(data);
|
|
// Print full response from idp
|
|
//dump(var = data, label = "makeAccessTokenRequest - Response from idp");
|
|
|
|
//echo('<a href="./index.cfm?refresh=#data.refresh_token#">Refresh token #data.refresh_token#</a>');
|
|
|
|
session.auth.expires_in = data.expires_in;
|
|
session.auth.refresh_expires_in = data.refresh_expires_in;
|
|
session.auth.access_token = data.access_token;
|
|
session.auth.refresh_token = data.refresh_token;
|
|
session.auth.id_token = data.id_token;
|
|
session.auth.token_type = data.token_type;
|
|
session.idp_response_content = structCopy(data);
|
|
|
|
//flush()
|
|
lock scope="application" type="readonly" timeout="1" {
|
|
token_data = (jwt.decode(data.access_token, application.idpCertificate,"RS256"));
|
|
}
|
|
writeOutput("jwt.decode Done");
|
|
writeOutput(getTickCount()-request.startTickCount);
|
|
writedump(token_data);
|
|
session.auth.login = token_data.preferred_username;
|
|
session.auth.wz = token_data.ClientID;
|
|
session.auth.fullname = token_data.name;
|
|
session.auth.groups = token_data.groups;
|
|
session.auth.session_state = token_data.session_state;
|
|
session.auth.sid = token_data.sid;
|
|
session.auth.auth_time = token_data.auth_time;
|
|
session.auth.exp = token_data.exp;
|
|
session.auth.iat = token_data.iat;
|
|
//session.sid = token_data.sid;
|
|
session.token_data = structCopy(token_data);
|
|
|
|
location(this.redirect_uri,false);
|
|
} catch (e) {echo('<br>****************** #e.message# : #e.detail# **********');}
|
|
|
|
} else {
|
|
strURL = idp.buildRedirectToAuthURL({"scope":'openid profile email',"state":session.auth_state,"allow_signup":false});
|
|
echo('<a href="#strURL#">self-made Auth link (idp) #strURL#</a> <br> <br>');
|
|
location(strUrl,false);
|
|
}
|
|
</cfscript>
|
|
</cfif>
|
|
|
|
</cflock><!--- session exclusive --->
|
|
|
|
|
|
|
|
|
|
<cfif structKeyExists(request, "auth")>
|
|
<cfinclude template="#ARGUMENTS.template#"/>
|
|
<cfelse>
|
|
<cfset currentDir = (
|
|
lCase(
|
|
replace(
|
|
getDirectoryFromPath(
|
|
replaceNoCase(
|
|
expandPath(
|
|
ARGUMENTS.template
|
|
),
|
|
getDirectoryFromPath(
|
|
getCurrentTemplatePath()
|
|
),
|
|
""
|
|
)
|
|
), "\", "/", "ALL"
|
|
)
|
|
)
|
|
)/>
|
|
<!--- <cfif currentDir EQ "saml/"><!--- note traling slash --->
|
|
<cfinclude template="#ARGUMENTS.template#"/>
|
|
<cfelse>
|
|
<cfinclude template="login.cfm" />
|
|
</cfif> --->
|
|
</cfif>
|
|
|
|
|
|
|
|
<cfreturn />
|
|
</cffunction>
|
|
|
|
|
|
<cffunction
|
|
name="OnRequestEnd"
|
|
access="public"
|
|
returntype="void"
|
|
output="true"
|
|
hint="Fires after the page processing is complete.">
|
|
<!--- Attention! Before CF9, OnrequestEnd is not executed in case of redirect. That is why we use session.save_login --->
|
|
|
|
<cfif structKeyExists(session, "save_login")>
|
|
<cfif session.save_login EQ "">
|
|
<!--- unset (expire) cookie --->
|
|
<cfcookie expires="-1" name="portalUser" value="">
|
|
<cfelse>
|
|
<!--- set persistent cookie with no expiration --->
|
|
<cfcookie expires="NEVER" name="portalUser" value="#encrypt(session.save_login,COOKIEENCKEY)#">
|
|
</cfif>
|
|
<cfset structDelete(session, "save_login")>
|
|
</cfif>
|
|
|
|
<cfreturn />
|
|
</cffunction>
|
|
|
|
<cffunction
|
|
name="getStand"
|
|
access="private"
|
|
returntype="string"
|
|
output="true"><!--- *** duplicated in svc-api --->
|
|
|
|
<cftry>
|
|
<cfquery name=qConfig>
|
|
select value as stand from config
|
|
where name='STAND'
|
|
</cfquery>
|
|
<cfreturn qConfig.stand/>
|
|
|
|
<cfcatch type="ANY">
|
|
<!--- do nothing, default will be returned --->
|
|
</cfcatch>
|
|
</cftry>
|
|
<cfreturn ""/>
|
|
</cffunction>
|
|
|
|
|
|
<cffunction
|
|
name="getDS"
|
|
access="private"
|
|
returntype="struct"
|
|
output="true"
|
|
hint="Configure data source from environment variables. Convention: data source name is an environment varialble prefix">
|
|
|
|
<cfargument name="dsname" type="string" required="true"/>
|
|
<cfargument name="prefix" type="string" default=#dsname#/>
|
|
|
|
<cfset var ds={}/>
|
|
|
|
<cftry>
|
|
<cfquery name="qTestDs" datasource=#arguments.dsname#>
|
|
select 1;
|
|
</cfquery>
|
|
|
|
<cfcatch type="any">
|
|
<cfset system = createObject("java", "java.lang.System")/>
|
|
|
|
<cfloop list="class,connectionString,database,driver,dbdriver,host,port,type,url,username,password,bundleName,bundleVersion,connectionLimit,liveTimeout,validate" item="field"><!--- driver vs dbdriver --->
|
|
<cfset var value=system.getEnv("#arguments.prefix#_#field#")/>
|
|
<cfif isDefined("value") AND len(value)>
|
|
<cfset structInsert(ds,field,value)/>
|
|
</cfif>
|
|
</cfloop>
|
|
<cfif structIsEmpty(ds)>
|
|
<cfthrow type="application" message="Datasource not configured" detail="Datasource not defined in the environment. Expected prefix is #arguments.prefix#"/>
|
|
</cfif>
|
|
|
|
<!--- test datasource (just to get exception if invalid) --->
|
|
<cftry>
|
|
<cfquery name="qTestDs" datasource=#ds#>
|
|
select 1;
|
|
</cfquery>
|
|
|
|
<cfcatch type="any">
|
|
<!--- <cfdump var=#ds#/><cfabort/> --->
|
|
<cfdump var=#arguments#/>
|
|
<cfdump var=#ds#/>
|
|
<cfrethrow/>
|
|
</cfcatch>
|
|
</cftry>
|
|
</cfcatch>
|
|
</cftry>
|
|
<!--- <cfdump var=#ds#/> --->
|
|
<cfreturn ds/>
|
|
</cffunction>
|
|
|
|
<cfscript>
|
|
//https://keycloak.nubes.ru/admin
|
|
//https://keycloak.nubes.ru/realms/SSH_CA/account //smishchuk@mgmt.nubes.ru
|
|
//https://stackoverflow.com/questions/28658735/what-are-keycloaks-oauth2-openid-connect-endpoints
|
|
//http://https://keycloak.nubes.ru/realms/SSH_CA/.well-known/openid-configuration
|
|
|
|
//https://keycloak.nubes.ru/realms/cloud/.well-known/openid-configuration тут ссылка
|
|
//https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/certs тут серт
|
|
//только надо его обрамить -----BEGIN CERTIFICATE-----
|
|
function getIdpCertificate(certUrl) {
|
|
|
|
var httpService = new http();
|
|
httpService.setMethod( "get" );
|
|
httpService.setCharset( "utf-8" );
|
|
httpService.setUrl(arguments.certUrl);
|
|
|
|
var result = httpService.send().getPrefix();
|
|
var status_code = result.ResponseHeader['Status_Code'];
|
|
|
|
if ('200' == status_code) {
|
|
var content = deserializeJson(result.FileContent);
|
|
//мы подразумеваем структуру конкретно KeyCloak
|
|
var key = "";
|
|
for (key in content.keys) {
|
|
if ("RSA" == key.kty AND "sig" == key.use) {
|
|
return "-----BEGIN CERTIFICATE-----" & key.x5c[1] & "-----END CERTIFICATE-----";
|
|
}
|
|
}
|
|
throw(message="RSA cetificate not found");
|
|
} else {
|
|
throw (message="Cannot obtain IDP certificate, request failed", detail="Status_Code #status_code#");
|
|
}
|
|
}
|
|
|
|
function auth() {
|
|
client_id = "payg-report.deck.nubes.ru"
|
|
client_secret = "********"
|
|
auth_endpoint = "https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/auth"
|
|
access_token_endpoint = "https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/token"
|
|
redirect_uri = "https://localhost/payg-report/"
|
|
|
|
jwt = new lib.jwt(client_secret);
|
|
idp = new lib.oauth2(client_id, client_secret, auth_endpoint, access_token_endpoint, redirect_uri);
|
|
|
|
// Get URL that the user will click on and grant our application all necessary permissions
|
|
strURL = idp.buildRedirectToAuthURL({"scope":'openid profile email',"state":createGUID().toString(),"allow_signup":true});
|
|
echo('<a href="#strURL#">self-made Auth link (idp) #strURL#</a> <br> <br>');
|
|
|
|
//redreshURL = github.refreshAccessTokenRequest();
|
|
|
|
if(structKeyExists(url, "code")) {
|
|
writedump(url.code);
|
|
// Request access token from idp with the authorization code that we got via the URL
|
|
data = deserializeJson(idp.makeAccessTokenRequest(url.code/*, aFormFields*/).content);
|
|
// Print full response from idp
|
|
dump(var = data, label = "makeAccessTokenRequest - Response from idp");
|
|
|
|
try {
|
|
//parsed=getParamsFromUrlString(data);
|
|
writedump(data);
|
|
echo('<a href="./index.cfm?refresh=#data.refresh_token#">Refresh token #data.refresh_token#</a>');
|
|
//https://keycloak.nubes.ru/realms/cloud/.well-known/openid-configuration тут ссылка
|
|
//https://keycloak.nubes.ru/realms/cloud/protocol/openid-connect/certs тут серт
|
|
//только надо его обрамить -----BEGIN CERTIFICATE-----
|
|
|
|
writedump(jwt.decode(data.access_token,"-----BEGIN CERTIFICATE-----
|
|
MIICmTCCAYECBgGG7361eDANBgkqhkiG9w0BAQsFADAQMQ4wDAYDVQQDDAVjbG91ZDAeFw0yMzAzMTcxMjEzMjRaFw0zMzAzMTcxMjE1MDRaMBAxDjAMBgNVBAMMBWNsb3VkMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn2segSPrciUFaD7Fa+brR9JdZknQr0F8zCzYQdTPsZJMtYnVPuE7MdZlbtGQ9n/HjcUst8FINjMONhzzbwVQSRZHH57pbAEm4r7fM3QDEAKXGERtDXhVLJGQSkg17X9zSWZs4RwkbxNsG6r+3/GT+U75lTG+/LKKzdG5OHK7Wwnrsiqy7sg3wAPFtlSwbebMdXJ70FosEI1SVhPRDn+LuVH3WINvijQU1GV2srWPwZl16fdXDele6NkvnC032l00pTbkVbgBLgySR0sPsI1s5AuMTT//nA0Xm5Ss40SwLJX2BYWyEALvEYdVt9wUZU2QXcZLLgmsCcFye4HjXa2CAwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQBlvkOLPRG6+tB30ZElwKSh/xrptnxgTf93RnS4IUDZebSolR5V/77PFAuqw4DTIlBNtJ6i1ffmyvtZIAMlm3sMZK0GuQ+SFq72DHxYzSmZ61cSS5YFKKyHcX3JIFMXYLmrw11GXt69V3xB8/Y0+BnBBpDD7ZKDdXdQa+9IWsERdTAIFk7ucwiG3BuTOJbzU1d+m8hNIVL4+dYR1TAnA9QPaJaQj0kUD0DYgXcIrrTFIQF0IPeB9I+eOyJ0FxJkukffX7QLVGFrc3+dVRHSSWmMpXi0B/VXT5DlsFDstEldH+/r3XnXAY6qadciyksSBRSXR4Rm9ZEq/xk+VmcxomWc
|
|
-----END CERTIFICATE-----","RS256"));
|
|
} catch (e) {echo('<br>****************** #e.message# : #e.detail# **********');}
|
|
|
|
}
|
|
|
|
if(structKeyExists(url, "refresh")) {
|
|
|
|
// With the refresh_token we can easly request a new access token
|
|
refreshedToken = deserializeJson(idp.refreshAccessTokenRequest(url.refresh).content);
|
|
dump(var = refreshedToken, label = "refreshAccessTokenRequest - Response from IDP");
|
|
|
|
echo('<br> <a href="./index.cfm?refresh=#refreshedToken.refresh_token#">Refresh token #refreshedToken.refresh_token#</a>')
|
|
writedump(jwt.decode(refreshedToken.access_token));
|
|
}
|
|
}
|
|
</cfscript>
|
|
</cfcomponent> |