Skip to content

Commit b213ba5

Browse files
committed
Merge branch 'master' into production
2 parents 40466ff + c0bd177 commit b213ba5

File tree

33 files changed

+1366
-133
lines changed

33 files changed

+1366
-133
lines changed

perun-base/src/main/java/cz/metacentrum/perun/core/api/CoreConfig.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,9 +96,12 @@ public void initBeansUtils() {
9696
private List<String> userInfoEndpointExtSourceFriendlyName;
9797
private String introspectionEndpointMfaAcrValue;
9898
private int mfaAuthTimeout;
99+
private int mfaAuthTimeoutPercentageForceLogIn;
99100
private boolean enforceMfa;
100101
private int idpLoginValidity;
101102
private List<String> idpLoginValidityExceptions;
103+
private int roleUpdateInterval;
104+
private boolean forceHTMLSanitization;
102105

103106
public int getGroupMaxConcurentGroupsToSynchronize() {
104107
return groupMaxConcurentGroupsToSynchronize;
@@ -798,6 +801,14 @@ public void setIntrospectionEndpointMfaAcrValue(String introspectionEndpointMfaA
798801
this.introspectionEndpointMfaAcrValue = introspectionEndpointMfaAcrValue;
799802
}
800803

804+
public boolean getForceHTMLSanitization() {
805+
return forceHTMLSanitization;
806+
}
807+
808+
public void setForceHTMLSanitization(boolean forceHTMLSanitization) {
809+
this.forceHTMLSanitization = forceHTMLSanitization;
810+
}
811+
801812
public int getMfaAuthTimeout() {
802813
return mfaAuthTimeout;
803814
}
@@ -806,6 +817,14 @@ public void setMfaAuthTimeout(int mfaAuthTimeout) {
806817
this.mfaAuthTimeout = mfaAuthTimeout;
807818
}
808819

820+
public int getMfaAuthTimeoutPercentageForceLogIn() {
821+
return mfaAuthTimeoutPercentageForceLogIn;
822+
}
823+
824+
public void setMfaAuthTimeoutPercentageForceLogIn(int mfaAuthTimeoutPercentageForceLogIn) {
825+
this.mfaAuthTimeoutPercentageForceLogIn = mfaAuthTimeoutPercentageForceLogIn;
826+
}
827+
809828
public boolean isEnforceMfa() {
810829
return enforceMfa;
811830
}
@@ -847,4 +866,12 @@ public Set<String> getBlockedLogins() {
847866

848867
return blockedLogins;
849868
}
869+
870+
public int getRoleUpdateInterval() {
871+
return roleUpdateInterval;
872+
}
873+
874+
public void setRoleUpdateInterval(int roleUpdateInterval) {
875+
this.roleUpdateInterval = roleUpdateInterval;
876+
}
850877
}

perun-base/src/main/java/cz/metacentrum/perun/core/api/PerunPrincipal.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ public class PerunPrincipal {
2020
private User user;
2121
// Contains principal's roles together with objects which specifies the role, e.g. VOADMIN -> list contains VO names
2222
private volatile AuthzRoles authzRoles = new AuthzRoles();
23+
// Time of the last update of roles
24+
private long rolesUpdatedAt = System.currentTimeMillis();
2325
// Map contains additional attributes, e.g. from authentication system
2426
private Map<String, String> additionalInformations = new HashMap<String, String>();
2527
// Specifies if the principal has initialized authZResolver
@@ -75,6 +77,7 @@ public String toString() {
7577
.append("user='").append((user != null ? user : "null")).append("', ")
7678
.append("extSourceName='").append(extSourceName).append("', ")
7779
.append("authzRoles='").append(authzRoles).append("', ")
80+
.append("rolesUpdatedAt='").append(rolesUpdatedAt).append("' ")
7881
.append("authzInitialized='").append(authzInitialized).append("']").toString();
7982
}
8083

@@ -142,6 +145,14 @@ public void setExtSourceLoa(int extSourceLoa) {
142145
this.extSourceLoa = extSourceLoa;
143146
}
144147

148+
public long getRolesUpdatedAt() {
149+
return rolesUpdatedAt;
150+
}
151+
152+
public void setRolesUpdatedAt(long rolesUpdatedAt) {
153+
this.rolesUpdatedAt = rolesUpdatedAt;
154+
}
155+
145156
@Override
146157
public int hashCode() {
147158
final int prime = 31;

perun-base/src/main/resources/perun-base.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
<property name="enginePrincipals" value="#{'${perun.engine.principals}'.split('\s*,\s*')}"/>
2222
<property name="generatedLoginNamespaces" value="#{'${perun.loginNamespace.generated}'.split('\s*,\s*')}"/>
2323
<property name="groupSynchronizationInterval" value="${perun.group.synchronization.interval}"/>
24+
<property name="roleUpdateInterval" value="${perun.roleUpdateInterval}"/>
2425
<property name="groupSynchronizationTimeout" value="${perun.group.synchronization.timeout}"/>
2526
<property name="groupStructureSynchronizationInterval" value="${perun.group.structure.synchronization.interval}"/>
2627
<property name="groupStructureSynchronizationTimeout" value="${perun.group.structure.synchronization.timeout}"/>
@@ -78,10 +79,12 @@
7879
<property name="userInfoEndpointExtSourceName" value="${perun.userInfoEndpoint.extSourceName}"/>
7980
<property name="userInfoEndpointExtSourceFriendlyName" value="#{'${perun.userInfoEndpoint.extSourceFriendlyName}'.split('\s*,\s*')}"/>
8081
<property name="mfaAuthTimeout" value="${perun.introspectionEndpoint.mfaAuthTimeout}"/>
82+
<property name="mfaAuthTimeoutPercentageForceLogIn" value="${perun.introspectionEndpoint.mfaAuthTimeoutPercentageForceLogIn}"/>
8183
<property name="enforceMfa" value="${perun.enforceMfa}"/>
8284
<property name="introspectionEndpointMfaAcrValue" value="${perun.introspectionEndpoint.mfaAcrValue}"/>
8385
<property name="idpLoginValidity" value="${perun.idpLoginValidity}"/>
8486
<property name="idpLoginValidityExceptions" value="#{'${perun.idpLoginValidityExceptions}'.split('\s*,\s*')}"/>
87+
<property name="forceHTMLSanitization" value="${perun.forceHtmlSanitization}"/>
8588
</bean>
8689

8790

@@ -147,6 +150,7 @@
147150
<prop key="perun.instanceName">LOCAL</prop>
148151
<prop key="perun.allowedCorsDomains"></prop>
149152
<prop key="perun.queryTimeout">-1</prop>
153+
<prop key="perun.roleUpdateInterval">5</prop>
150154
<prop key="perun.defaultLoa.idp">2</prop>
151155
<prop key="perun.attributesToSearchUsersAndMembersBy">urn:perun:user:attribute-def:def:preferredMail, urn:perun:member:attribute-def:def:mail</prop>
152156
<prop key="perun.attributesToAnonymize"></prop>
@@ -181,8 +185,10 @@
181185
<prop key="perun.userInfoEndpoint.extSourceName">target_issuer</prop>
182186
<prop key="perun.userInfoEndpoint.extSourceFriendlyName">target_backend, display_name, text</prop>
183187
<prop key="perun.introspectionEndpoint.mfaAuthTimeout">1440</prop>
188+
<prop key="perun.introspectionEndpoint.mfaAuthTimeoutPercentageForceLogIn">75</prop>
184189
<prop key="perun.introspectionEndpoint.mfaAcrValue">https://refeds.org/profile/mfa</prop>
185190
<prop key="perun.enforceMfa">false</prop>
191+
<prop key="perun.forceHtmlSanitization">false</prop>
186192
</props>
187193
</property>
188194
</bean>

perun-base/src/test/resources/test-schema.sql

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
-- database version 3.2.14 (don't forget to update insert statement at the end of file)
1+
-- database version 3.2.15 (don't forget to update insert statement at the end of file)
22
CREATE EXTENSION IF NOT EXISTS "unaccent";
33
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
44

@@ -1905,7 +1905,7 @@ create index idx_fk_attr_critops ON attribute_critical_actions(attr_id);
19051905
create index app_state_idx ON application (state);
19061906

19071907
-- set initial Perun DB version
1908-
insert into configurations values ('DATABASE VERSION','3.2.14');
1908+
insert into configurations values ('DATABASE VERSION','3.2.15');
19091909
-- insert membership types
19101910
insert into membership_types (id, membership_type, description) values (1, 'DIRECT', 'Member is directly added into group');
19111911
insert into membership_types (id, membership_type, description) values (2, 'INDIRECT', 'Member is added indirectly through UNION relation');

perun-cli-python/perun/oidc/__init__.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ class PerunInstance(str, Enum):
2828
cesnet = "cesnet",
2929
idm = "idm",
3030
idm_test = "idm-test",
31-
perun_dev = "perun-dev"
31+
perun_dev = "perun-dev",
32+
elixir = "elixir"
3233

3334

3435
class DeviceCodeOAuth:
@@ -85,6 +86,13 @@ def __init__(self, perun_instance: PerunInstance, encryption_password: str, mfa:
8586
'perun_api_url': 'https://idm.ics.muni.cz/oauth/rpc',
8687
'mfa': True
8788
},
89+
PerunInstance.elixir: {
90+
'metadata_url': 'https://login.elixir-czech.org/oidc/.well-known/openid-configuration',
91+
'client_id': 'da97db9f-b511-4c72-b71f-daab24b86884',
92+
'scopes': 'openid perun_api perun_admin offline_access profile authn_details',
93+
'perun_api_url': 'https://elixir-api.aai.lifescience-ri.eu/oauth/rpc',
94+
'mfa': True
95+
},
8896
# PerunInstance.idm_satosa: {
8997
# 'metadata_url': 'https://proxy.aai.muni.cz/OIDC/.well-known/openid-configuration',
9098
# 'client_id': '5a730abc-6553-4fc4-af9a-21c75c46e0c2',
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package cz.metacentrum.perun.core.api.exceptions;
2+
3+
import cz.metacentrum.perun.core.api.exceptions.rt.PerunRuntimeException;
4+
5+
/**
6+
* This exception is thrown when principal has roles always requiring MFA and the auth time is older than the limit defined in the config
7+
* @author Jakub Hejda <[email protected]>
8+
*/
9+
public class MfaRoleTimeoutException extends PerunRuntimeException {
10+
static final long serialVersionUID = 0;
11+
12+
public MfaRoleTimeoutException(String message) {
13+
super(message);
14+
}
15+
16+
public MfaRoleTimeoutException(String message, Throwable cause) {
17+
super(message, cause);
18+
}
19+
20+
public MfaRoleTimeoutException(Throwable cause) {
21+
super(cause);
22+
}
23+
}

perun-core/src/main/java/cz/metacentrum/perun/core/api/exceptions/MfaTimeoutException.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
import cz.metacentrum.perun.core.api.exceptions.rt.PerunRuntimeException;
44

5+
/**
6+
* This exception is thrown when principal is performing MFA-requiring action and the auth time is older than the limit defined in the config
7+
* @author Jakub Hejda <[email protected]>
8+
*/
59
public class MfaTimeoutException extends PerunRuntimeException {
610
static final long serialVersionUID = 0;
711

perun-core/src/main/java/cz/metacentrum/perun/core/blImpl/AuthzResolverBlImpl.java

Lines changed: 70 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
import cz.metacentrum.perun.core.api.exceptions.MfaInvalidRolesException;
5656
import cz.metacentrum.perun.core.api.exceptions.MfaPrivilegeException;
5757
import cz.metacentrum.perun.core.api.exceptions.MfaRolePrivilegeException;
58+
import cz.metacentrum.perun.core.api.exceptions.MfaRoleTimeoutException;
5859
import cz.metacentrum.perun.core.api.exceptions.MfaTimeoutException;
5960
import cz.metacentrum.perun.core.api.exceptions.PolicyNotExistsException;
6061
import cz.metacentrum.perun.core.api.exceptions.ResourceNotExistsException;
@@ -95,6 +96,7 @@
9596
import java.util.Map;
9697
import java.util.Objects;
9798
import java.util.Set;
99+
import java.util.concurrent.TimeUnit;
98100
import java.util.function.BiFunction;
99101
import java.util.function.Function;
100102
import java.util.stream.Collectors;
@@ -138,6 +140,8 @@ public static boolean authorized(PerunSession sess, String policyDefinition, Lis
138140
refreshAuthz(sess);
139141
}
140142

143+
periodicCheckAuthz(sess);
144+
141145
// If the user has no roles, deny access
142146
if (sess.getPerunPrincipal().getRoles() == null) {
143147
return false;
@@ -162,6 +166,20 @@ public static boolean authorized(PerunSession sess, String policyDefinition, Lis
162166
return resolveAuthorization(sess, policyRoles, mapOfBeans);
163167
}
164168

169+
/**
170+
* If the last check was earlier than the set interval then updates the roles.
171+
*
172+
* @param sess session
173+
*/
174+
private static void periodicCheckAuthz(PerunSession sess) {
175+
if (System.currentTimeMillis() - sess.getPerunPrincipal().getRolesUpdatedAt() >= TimeUnit.MINUTES.toMillis(BeansUtils.getCoreConfig().getRoleUpdateInterval())) {
176+
log.debug("Periodic update authz roles for session {}.", sess);
177+
178+
refreshAuthz(sess);
179+
sess.getPerunPrincipal().setRolesUpdatedAt(System.currentTimeMillis());
180+
}
181+
}
182+
165183
/**
166184
* Checks authorization according to MFA rules.
167185
*
@@ -271,6 +289,8 @@ public static boolean authorizedToManageRole(PerunSession sess, PerunBean object
271289
refreshAuthz(sess);
272290
}
273291

292+
periodicCheckAuthz(sess);
293+
274294
// If the user has no roles, deny access
275295
if (sess.getPerunPrincipal().getRoles() == null) {
276296
return false;
@@ -302,6 +322,8 @@ public static boolean authorizedToReadRole(PerunSession sess, PerunBean object,
302322
refreshAuthz(sess);
303323
}
304324

325+
periodicCheckAuthz(sess);
326+
305327
// If the user has no roles, deny access
306328
if (sess.getPerunPrincipal().getRoles() == null) {
307329
return false;
@@ -1853,6 +1875,8 @@ private static boolean hasAccessByDefault(PerunSession sess, AttributeAction act
18531875
refreshAuthz(sess);
18541876
}
18551877

1878+
periodicCheckAuthz(sess);
1879+
18561880
if (sess.getPerunPrincipal().getRoles() == null || sess.getPerunPrincipal().getRoles().isEmpty()) {
18571881
return false;
18581882
}
@@ -4272,7 +4296,11 @@ private static void checkMfaForHavingRole(PerunSession sess, AuthzRoles roles) {
42724296
}
42734297

42744298
if (!requireMfaRoles.isEmpty() && !sess.getPerunPrincipal().getRoles().hasRole(Role.MFA)) {
4275-
throw new MfaRolePrivilegeException(sess, requireMfaRoles.get(0));
4299+
if (checkAuthValidityForMFA(sess)) {
4300+
throw new MfaRolePrivilegeException(sess, requireMfaRoles.get(0));
4301+
} else {
4302+
throw new MfaRoleTimeoutException("Your MFA timestamp is not valid anymore, you'll need to reauthenticate");
4303+
}
42764304
}
42774305
}
42784306

@@ -4368,6 +4396,27 @@ private static boolean isAuthorizedByMfa(PerunSession sess, boolean throwError)
43684396
return false;
43694397
}
43704398

4399+
if (checkAuthValidityForMFA(sess)) {
4400+
// true if user has MFA and it is still valid
4401+
return sessionHasMfa(sess);
4402+
} else {
4403+
if (!throwError) return false;
4404+
if (sessionHasMfa(sess)) {
4405+
// MFA is no longer valid
4406+
throw new MfaTimeoutException("Your MFA timestamp is not valid anymore, you'll need to reauthenticate");
4407+
} else {
4408+
// user is authenticated by SFA but the mfa timeout would cause an error, so we need to reauthenticate this user
4409+
throw new MfaTimeoutException("Your single factor authentication timestamp is not valid anymore, you'll need to reauthenticate");
4410+
}
4411+
}
4412+
}
4413+
4414+
/**
4415+
* Check if the auth time + mfa timeout (reduced by percentage from config) > the current time
4416+
* @param sess session
4417+
* @return true if the auth timestamp is not too old to perform step-up
4418+
*/
4419+
private static boolean checkAuthValidityForMFA(PerunSession sess) {
43714420
String returnedAuthTime = sess.getPerunPrincipal().getAdditionalInformations().get(AUTH_TIME);
43724421
Instant parsedReturnedAuthTime;
43734422
try {
@@ -4380,21 +4429,27 @@ private static boolean isAuthorizedByMfa(PerunSession sess, boolean throwError)
43804429
}
43814430

43824431
long mfaTimeoutInSec = Duration.ofMinutes(BeansUtils.getCoreConfig().getMfaAuthTimeout()).getSeconds();
4383-
Instant mfaValidUntil = parsedReturnedAuthTime.plusSeconds(mfaTimeoutInSec);
4384-
4385-
// check if the auth time + mfa timeout > the current time
4386-
if (mfaValidUntil.isAfter(Instant.now())) {
4387-
// if user has MFA and it is still valid
4388-
return sess.getPerunPrincipal().getAdditionalInformations().containsKey(ACR_MFA);
4389-
} else {
4390-
if (!throwError) return false;
4391-
if (sess.getPerunPrincipal().getAdditionalInformations().containsKey(ACR_MFA)) {
4392-
// MFA is no longer valid
4393-
throw new MfaTimeoutException("Your MFA timestamp " + returnedAuthTime + " is not valid anymore, you'll need to reauthenticate");
4394-
} else {
4395-
// user is authenticated by SFA but the mfa timeout would cause an error, so we need to reauthenticate this user
4396-
throw new MfaTimeoutException("Your single factor authentication timestamp " + returnedAuthTime + " is not valid anymore, you'll need to reauthenticate");
4432+
double mfaTimeoutPercentage = 1;
4433+
// if the current session is SFA, we want to force log in with both factors earlier (e.g. 75% of mfaAuthTimeout) due to the first executed MFA since authentication time
4434+
// -> we want to avoid situation when the validity is e.g. 60 minutes, user executes MFA (just second factor) after 59 minutes and after one minute he/she would need to log in again with both factors
4435+
if (!sessionHasMfa(sess)) {
4436+
mfaTimeoutPercentage = (double) BeansUtils.getCoreConfig().getMfaAuthTimeoutPercentageForceLogIn() / 100;
4437+
if (mfaTimeoutPercentage < 0 || mfaTimeoutPercentage > 1) {
4438+
throw new InternalErrorException("MFA auth timestamp percentage force logout " + mfaTimeoutPercentage + " is not between 0 and 100");
43974439
}
43984440
}
4441+
4442+
Instant mfaValidUntil = parsedReturnedAuthTime.plusSeconds((long) (mfaTimeoutInSec * mfaTimeoutPercentage));
4443+
4444+
return mfaValidUntil.isAfter(Instant.now());
4445+
}
4446+
4447+
/**
4448+
* Check if the perun principal contains acr_mfa. It means that user has been authenticated by MFA.
4449+
* @param sess session
4450+
* @return true if principal contains acr_mfa
4451+
*/
4452+
private static boolean sessionHasMfa(PerunSession sess) {
4453+
return sess.getPerunPrincipal().getAdditionalInformations().containsKey(ACR_MFA);
43994454
}
44004455
}

0 commit comments

Comments
 (0)