From 6e26490880ecc25a5c41930524f1432aa3ca39d4 Mon Sep 17 00:00:00 2001 From: JamesChenX Date: Sun, 14 Jan 2024 16:43:51 +0800 Subject: [PATCH] Support LDAP-based user authentication #1371 --- .../domain/session/bo/UserPermissionInfo.java | 17 +- .../session/repository/UserRepository.java | 7 +- .../HttpSessionIdentityAccessManager.java | 127 ++++++++ .../JwtSessionIdentityAccessManager.java | 135 +++++++++ .../LdapSessionIdentityAccessManager.java | 194 ++++++++++++ .../NoopSessionIdentityAccessManager.java | 36 +++ .../PasswordSessionIdentityAccessManager.java | 80 +++++ ...essionIdentityAccessManagementSupport.java | 31 +- .../service/SessionIdentityAccessManager.java | 277 ++---------------- .../session/service/SessionService.java | 22 +- .../domain/session/service/UserService.java | 6 +- .../gateway/infra/ldap/asn1/BerBuffer.java | 9 +- .../session/service/SessionServiceTests.java | 1 + .../turms/gateway/infra/ldap/FilterTest.java | 8 +- .../IdentityAccessManagementType.java | 3 +- .../IdentityAccessManagementProperties.java | 5 + ...entityAccessManagementAdminProperties.java | 55 ++++ ...dapIdentityAccessManagementProperties.java | 48 +++ ...dentityAccessManagementUserProperties.java | 58 ++++ ...operties-metadata-with-property-value.json | 81 ++++- .../resources/turms-properties-metadata.json | 73 ++++- ...urms-properties-only-mutable-metadata.json | 4 + 22 files changed, 990 insertions(+), 287 deletions(-) create mode 100644 turms-gateway/src/main/java/im/turms/gateway/domain/session/service/HttpSessionIdentityAccessManager.java create mode 100644 turms-gateway/src/main/java/im/turms/gateway/domain/session/service/JwtSessionIdentityAccessManager.java create mode 100644 turms-gateway/src/main/java/im/turms/gateway/domain/session/service/LdapSessionIdentityAccessManager.java create mode 100644 turms-gateway/src/main/java/im/turms/gateway/domain/session/service/NoopSessionIdentityAccessManager.java create mode 100644 turms-gateway/src/main/java/im/turms/gateway/domain/session/service/PasswordSessionIdentityAccessManager.java create mode 100644 turms-server-common/src/main/java/im/turms/server/common/infra/property/env/gateway/identityaccessmanagement/ldap/LdapIdentityAccessManagementAdminProperties.java create mode 100644 turms-server-common/src/main/java/im/turms/server/common/infra/property/env/gateway/identityaccessmanagement/ldap/LdapIdentityAccessManagementProperties.java create mode 100644 turms-server-common/src/main/java/im/turms/server/common/infra/property/env/gateway/identityaccessmanagement/ldap/LdapIdentityAccessManagementUserProperties.java diff --git a/turms-gateway/src/main/java/im/turms/gateway/domain/session/bo/UserPermissionInfo.java b/turms-gateway/src/main/java/im/turms/gateway/domain/session/bo/UserPermissionInfo.java index 7d2b3bcb13..2fca0f8c1a 100644 --- a/turms-gateway/src/main/java/im/turms/gateway/domain/session/bo/UserPermissionInfo.java +++ b/turms-gateway/src/main/java/im/turms/gateway/domain/session/bo/UserPermissionInfo.java @@ -20,7 +20,10 @@ import java.util.Collections; import java.util.Set; +import reactor.core.publisher.Mono; + import im.turms.server.common.access.client.dto.request.TurmsRequest; +import im.turms.server.common.access.client.dto.request.TurmsRequestTypePool; import im.turms.server.common.access.common.ResponseStatusCode; /** @@ -30,7 +33,19 @@ public record UserPermissionInfo( ResponseStatusCode authenticationCode, Set permissions ) { + + public static final UserPermissionInfo GRANTED_WITH_ALL_PERMISSIONS = + new UserPermissionInfo(ResponseStatusCode.OK, TurmsRequestTypePool.ALL); + public static final Mono GRANTED_WITH_ALL_PERMISSIONS_MONO = + Mono.just(GRANTED_WITH_ALL_PERMISSIONS); + public static final UserPermissionInfo LOGIN_AUTHENTICATION_FAILED = + new UserPermissionInfo(ResponseStatusCode.LOGIN_AUTHENTICATION_FAILED); + public static final Mono LOGIN_AUTHENTICATION_FAILED_MONO = + Mono.just(LOGIN_AUTHENTICATION_FAILED); + public static final Mono LOGGING_IN_USER_NOT_ACTIVE_MONO = + Mono.just(new UserPermissionInfo(ResponseStatusCode.LOGGING_IN_USER_NOT_ACTIVE)); + public UserPermissionInfo(ResponseStatusCode authenticationCode) { this(authenticationCode, Collections.emptySet()); } -} +} \ No newline at end of file diff --git a/turms-gateway/src/main/java/im/turms/gateway/domain/session/repository/UserRepository.java b/turms-gateway/src/main/java/im/turms/gateway/domain/session/repository/UserRepository.java index f18dbeaf9c..0ff42cfd5a 100644 --- a/turms-gateway/src/main/java/im/turms/gateway/domain/session/repository/UserRepository.java +++ b/turms-gateway/src/main/java/im/turms/gateway/domain/session/repository/UserRepository.java @@ -17,6 +17,7 @@ package im.turms.gateway.domain.session.repository; +import lombok.Getter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Repository; @@ -35,10 +36,14 @@ @Repository public class UserRepository extends BaseRepository { + @Getter + private final boolean enabled; + public UserRepository( @Autowired( required = false) @Qualifier("userMongoClient") TurmsMongoClient mongoClient) { super(mongoClient, User.class); + enabled = mongoClient != null; } public Mono findPassword(Long userId) { @@ -57,4 +62,4 @@ public Mono isActiveAndNotDeleted(Long userId) { return mongoClient.exists(User.class, filter); } -} +} \ No newline at end of file diff --git a/turms-gateway/src/main/java/im/turms/gateway/domain/session/service/HttpSessionIdentityAccessManager.java b/turms-gateway/src/main/java/im/turms/gateway/domain/session/service/HttpSessionIdentityAccessManager.java new file mode 100644 index 0000000000..9305253215 --- /dev/null +++ b/turms-gateway/src/main/java/im/turms/gateway/domain/session/service/HttpSessionIdentityAccessManager.java @@ -0,0 +1,127 @@ +/* + * Copyright (C) 2019 The Turms Project + * https://github.com/turms-im/turms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.turms.gateway.domain.session.service; + +import java.time.Duration; +import java.util.Map; +import java.util.Set; + +import io.netty.handler.codec.http.HttpMethod; +import reactor.core.publisher.Mono; +import reactor.netty.http.client.HttpClient; + +import im.turms.gateway.access.client.common.authorization.policy.Policy; +import im.turms.gateway.access.client.common.authorization.policy.PolicyDeserializer; +import im.turms.gateway.access.client.common.authorization.policy.PolicyManager; +import im.turms.gateway.domain.session.bo.UserLoginInfo; +import im.turms.gateway.domain.session.bo.UserPermissionInfo; +import im.turms.server.common.access.common.ResponseStatusCode; +import im.turms.server.common.infra.collection.CollectionUtil; +import im.turms.server.common.infra.json.JsonUtil; +import im.turms.server.common.infra.lang.StringUtil; +import im.turms.server.common.infra.property.env.gateway.identityaccessmanagement.http.HttpAuthenticationResponseExpectationProperties; +import im.turms.server.common.infra.property.env.gateway.identityaccessmanagement.http.HttpIdentityAccessManagementProperties; +import im.turms.server.common.infra.property.env.gateway.identityaccessmanagement.http.HttpIdentityAccessManagementRequestProperties; +import im.turms.server.common.infra.validation.Validator; + +/** + * @author James Chen + */ +public class HttpSessionIdentityAccessManager implements SessionIdentityAccessManagementSupport { + + private final HttpClient httpIdentityAccessManagementClient; + private final HttpMethod httpIdentityAccessManagementHttpMethod; + private final Set httpAuthenticationExpectedStatusCodes; + private final Map httpAuthenticationExpectedHeaders; + private final Map httpAuthenticationExpectedBodyFields; + + private final PolicyManager policyManager; + + public HttpSessionIdentityAccessManager( + HttpIdentityAccessManagementProperties httpProperties, + PolicyManager policyManager) { + HttpIdentityAccessManagementRequestProperties requestProperties = + httpProperties.getRequest(); + HttpAuthenticationResponseExpectationProperties responseExpectationProperties = + httpProperties.getAuthentication() + .getResponseExpectation(); + String url = requestProperties.getUrl(); + Exception exception = Validator.url(url); + if (exception != null) { + throw new IllegalArgumentException( + "Illegal HTTP URL: " + + url, + exception); + } + this.httpIdentityAccessManagementClient = HttpClient.create() + .baseUrl(url) + .headers(entries -> { + for (Map.Entry entry : requestProperties.getHeaders() + .entrySet()) { + entries.add(entry.getKey(), entry.getValue()); + } + }) + .responseTimeout(Duration.ofMillis(requestProperties.getTimeoutMillis())); + this.httpIdentityAccessManagementHttpMethod = + HttpMethod.valueOf(requestProperties.getHttpMethod() + .name()); + this.httpAuthenticationExpectedStatusCodes = responseExpectationProperties.getStatusCodes(); + this.httpAuthenticationExpectedHeaders = responseExpectationProperties.getHeaders(); + this.httpAuthenticationExpectedBodyFields = responseExpectationProperties.getBodyFields(); + + this.policyManager = policyManager; + } + + @Override + public Mono verifyAndGrant(UserLoginInfo userLoginInfo) { + return httpIdentityAccessManagementClient.request(httpIdentityAccessManagementHttpMethod) + .send(Mono.fromCallable(() -> JsonUtil.write(userLoginInfo))) + .responseSingle((response, bodyBufferMono) -> { + if (!StringUtil.matchLatin1(response.status() + .toString(), httpAuthenticationExpectedStatusCodes)) { + return UserPermissionInfo.LOGIN_AUTHENTICATION_FAILED_MONO; + } + for (Map.Entry entry : httpAuthenticationExpectedHeaders + .entrySet()) { + if (!entry.getValue() + .equals(response.responseHeaders() + .get(entry.getKey()))) { + return UserPermissionInfo.LOGIN_AUTHENTICATION_FAILED_MONO; + } + } + return bodyBufferMono.asInputStream() + .map(inputStream -> { + Map map; + Policy policy; + try { + map = JsonUtil.readStringObjectMapValue(inputStream); + policy = PolicyDeserializer.parse(map); + } catch (Exception e) { + throw new IllegalArgumentException("Illegal request body", e); + } + if (!CollectionUtil.containsAllLooseComparison(map, + httpAuthenticationExpectedBodyFields)) { + return UserPermissionInfo.LOGIN_AUTHENTICATION_FAILED; + } + return new UserPermissionInfo( + ResponseStatusCode.OK, + policyManager.findAllowedRequestTypes(policy)); + }); + }); + } +} \ No newline at end of file diff --git a/turms-gateway/src/main/java/im/turms/gateway/domain/session/service/JwtSessionIdentityAccessManager.java b/turms-gateway/src/main/java/im/turms/gateway/domain/session/service/JwtSessionIdentityAccessManager.java new file mode 100644 index 0000000000..857a658dc6 --- /dev/null +++ b/turms-gateway/src/main/java/im/turms/gateway/domain/session/service/JwtSessionIdentityAccessManager.java @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2019 The Turms Project + * https://github.com/turms-im/turms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.turms.gateway.domain.session.service; + +import java.security.NoSuchAlgorithmException; +import java.util.Date; +import java.util.Map; + +import reactor.core.publisher.Mono; + +import im.turms.gateway.access.client.common.authorization.policy.IllegalPolicyException; +import im.turms.gateway.access.client.common.authorization.policy.Policy; +import im.turms.gateway.access.client.common.authorization.policy.PolicyDeserializer; +import im.turms.gateway.access.client.common.authorization.policy.PolicyManager; +import im.turms.gateway.domain.session.bo.UserLoginInfo; +import im.turms.gateway.domain.session.bo.UserPermissionInfo; +import im.turms.server.common.access.common.ResponseStatusCode; +import im.turms.server.common.infra.collection.CollectionUtil; +import im.turms.server.common.infra.lang.StringUtil; +import im.turms.server.common.infra.logging.core.logger.Logger; +import im.turms.server.common.infra.logging.core.logger.LoggerFactory; +import im.turms.server.common.infra.property.env.gateway.identityaccessmanagement.jwt.JwtAlgorithmProperties; +import im.turms.server.common.infra.property.env.gateway.identityaccessmanagement.jwt.JwtIdentityAccessManagementProperties; +import im.turms.server.common.infra.security.jwt.Jwt; +import im.turms.server.common.infra.security.jwt.JwtManager; +import im.turms.server.common.infra.security.jwt.JwtPayload; +import im.turms.server.common.infra.security.jwt.exception.InvalidJwtException; +import im.turms.server.common.infra.security.jwt.exception.JwtSignatureVerificationException; + +import static im.turms.gateway.domain.session.bo.UserPermissionInfo.*; + +/** + * @author James Chen + */ +public class JwtSessionIdentityAccessManager implements SessionIdentityAccessManagementSupport { + + private static final Logger LOGGER = + LoggerFactory.getLogger(JwtSessionIdentityAccessManager.class); + + private final JwtManager jwtManager; + private final PolicyManager policyManager; + private final Map jwtAuthenticationExpectedCustomPayloadClaims; + + public JwtSessionIdentityAccessManager( + JwtIdentityAccessManagementProperties jwtProperties, + PolicyManager policyManager) { + jwtAuthenticationExpectedCustomPayloadClaims = jwtProperties.getAuthentication() + .getExpectation() + .getCustomPayloadClaims(); + JwtAlgorithmProperties jwtAlgorithmProperties = jwtProperties.getAlgorithm(); + jwtManager = new JwtManager( + jwtProperties.getVerification(), + jwtAlgorithmProperties.getRsa256(), + jwtAlgorithmProperties.getRsa384(), + jwtAlgorithmProperties.getRsa512(), + jwtAlgorithmProperties.getPs256(), + jwtAlgorithmProperties.getPs384(), + jwtAlgorithmProperties.getPs512(), + jwtAlgorithmProperties.getEcdsa256(), + jwtAlgorithmProperties.getEcdsa384(), + jwtAlgorithmProperties.getEcdsa512(), + jwtAlgorithmProperties.getHmac256(), + jwtAlgorithmProperties.getHmac384(), + jwtAlgorithmProperties.getHmac512()); + LOGGER.info("Supported algorithms for JWT: {}", jwtManager.getSupportedAlgorithmNames()); + + this.policyManager = policyManager; + } + + @Override + public Mono verifyAndGrant(UserLoginInfo userLoginInfo) { + Long userId = userLoginInfo.userId(); + String jwtToken = userLoginInfo.password(); + if (StringUtil.isBlank(jwtToken)) { + return Mono.error( + new IllegalArgumentException("Invalid JWT token: JWT must not be blank")); + } + Jwt jwt; + try { + jwt = jwtManager.decode(jwtToken); + } catch (InvalidJwtException | NoSuchAlgorithmException + | JwtSignatureVerificationException e) { + return Mono.error(new IllegalArgumentException("Invalid JWT token", e)); + } + JwtPayload payload = jwt.payload(); + String subject = payload.subject(); + if (subject == null) { + return Mono.error(new IllegalArgumentException( + "Invalid JWT token: the sub claim in the payload must exist")); + } + if (!subject.equals(userId.toString())) { + return LOGIN_AUTHENTICATION_FAILED_MONO; + } + Date expiresAt = payload.expiresAt(); + Date notBefore = payload.notBefore(); + boolean hasExpiresAt = expiresAt != null; + boolean hasNotBefore = notBefore != null; + if (hasExpiresAt || hasNotBefore) { + long now = System.currentTimeMillis(); + if ((hasExpiresAt && expiresAt.getTime() <= now) + || (hasNotBefore && notBefore.getTime() > now)) { + return LOGIN_AUTHENTICATION_FAILED_MONO; + } + } + Map customClaims = payload.customClaims(); + if (!CollectionUtil.containsAllLooseComparison(customClaims, + jwtAuthenticationExpectedCustomPayloadClaims)) { + return LOGIN_AUTHENTICATION_FAILED_MONO; + } + Policy policy; + try { + policy = PolicyDeserializer.parse(customClaims); + } catch (IllegalPolicyException e) { + return Mono.error(new IllegalArgumentException("Invalid JWT token", e)); + } + return Mono.just(new UserPermissionInfo( + ResponseStatusCode.OK, + policyManager.findAllowedRequestTypes(policy))); + } +} \ No newline at end of file diff --git a/turms-gateway/src/main/java/im/turms/gateway/domain/session/service/LdapSessionIdentityAccessManager.java b/turms-gateway/src/main/java/im/turms/gateway/domain/session/service/LdapSessionIdentityAccessManager.java new file mode 100644 index 0000000000..989fdb76d1 --- /dev/null +++ b/turms-gateway/src/main/java/im/turms/gateway/domain/session/service/LdapSessionIdentityAccessManager.java @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2019 The Turms Project + * https://github.com/turms-im/turms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.turms.gateway.domain.session.service; + +import java.util.List; + +import reactor.core.publisher.Mono; + +import im.turms.gateway.domain.session.bo.UserLoginInfo; +import im.turms.gateway.domain.session.bo.UserPermissionInfo; +import im.turms.gateway.infra.ldap.LdapClient; +import im.turms.gateway.infra.ldap.element.operation.search.DerefAliases; +import im.turms.gateway.infra.ldap.element.operation.search.Scope; +import im.turms.gateway.infra.ldap.element.operation.search.SearchRequest; +import im.turms.gateway.infra.ldap.element.operation.search.SearchResultEntry; +import im.turms.server.common.access.common.ResponseStatusCode; +import im.turms.server.common.infra.exception.ResponseException; +import im.turms.server.common.infra.lang.StringUtil; +import im.turms.server.common.infra.logging.core.logger.Logger; +import im.turms.server.common.infra.logging.core.logger.LoggerFactory; +import im.turms.server.common.infra.property.env.gateway.identityaccessmanagement.ldap.LdapIdentityAccessManagementAdminProperties; +import im.turms.server.common.infra.property.env.gateway.identityaccessmanagement.ldap.LdapIdentityAccessManagementProperties; +import im.turms.server.common.infra.property.env.gateway.identityaccessmanagement.ldap.LdapIdentityAccessManagementUserProperties; +import im.turms.server.common.infra.reactor.TaskScheduler; +import im.turms.server.common.infra.time.DurationConst; + +import static im.turms.gateway.domain.session.bo.UserPermissionInfo.GRANTED_WITH_ALL_PERMISSIONS_MONO; +import static im.turms.gateway.domain.session.bo.UserPermissionInfo.LOGGING_IN_USER_NOT_ACTIVE_MONO; +import static im.turms.gateway.domain.session.bo.UserPermissionInfo.LOGIN_AUTHENTICATION_FAILED_MONO; + +/** + * @author James Chen + */ +public class LdapSessionIdentityAccessManager implements SessionIdentityAccessManagementSupport { + + private static final Logger LOGGER = + LoggerFactory.getLogger(LdapSessionIdentityAccessManager.class); + + private final String baseDn; + private final String userSearchFilter; + + private final LdapClient adminLdapClient; + private final LdapClient userLdapClient; + + private final TaskScheduler clientBindTaskScheduler; + + public LdapSessionIdentityAccessManager(LdapIdentityAccessManagementProperties properties) { + baseDn = properties.getBaseDn(); + + LdapIdentityAccessManagementAdminProperties adminProperties = properties.getAdmin(); + adminLdapClient = new LdapClient( + adminProperties.getHost(), + adminProperties.getPort(), + adminProperties.getSsl()); + + LdapIdentityAccessManagementUserProperties userProperties = properties.getUser(); + userSearchFilter = userProperties.getSearchFilter(); + if (!userSearchFilter.contains( + LdapIdentityAccessManagementUserProperties.SEARCH_FILTER_PLACEHOLDER_USER_ID)) { + throw new IllegalArgumentException( + "The user search filter must contain the placeholder for user ID"); + } + userLdapClient = new LdapClient( + userProperties.getHost(), + userProperties.getPort(), + userProperties.getSsl()); + + String adminUsername = adminProperties.getUsername(); + LOGGER.info("Checking the LDAP server for the admin: " + + adminUsername); + LOGGER.info("Checking the LDAP server for users"); + Mono checkAdminLdapServer = + adminLdapClient.bind(false, adminUsername, adminProperties.getPassword()) + .flatMap(authenticated -> { + if (!authenticated) { + return Mono.error(new RuntimeException( + "Failed to bind to the LDAP server for the admin \"" + + adminUsername + + "\" because of invalid credentials")); + } + return Mono.empty(); + }) + .then(Mono.defer(() -> adminLdapClient + .search(baseDn, + Scope.SINGLE_LEVEL, + DerefAliases.ALWAYS, + 1, + 0, + false, + SearchRequest.NO_ATTRIBUTES, + "objectClass=*") + .flatMap(searchResult -> searchResult.isSuccess() + ? Mono.empty() + : Mono.error(new RuntimeException( + "Failed to search on the LDAP server for the admin \"" + + adminUsername + + "\" with the result code: " + + searchResult.getResultCode()))) + .then())) + .doOnSuccess( + result -> LOGGER.info("Checked the LDAP server for the admin: \"" + + adminUsername + + "\"")) + .onErrorMap(throwable -> new RuntimeException( + "Failed to check the LDAP server for the admin: \"" + + adminUsername + + "\"", + throwable)); + + Mono checkedUserLdapServer = userLdapClient.bind(true, "", "") + // We don't need to check its response because we just need to + // ensure it can connect, and the communication works fine. + .doOnSuccess(authenticated -> LOGGER.info("Checked the LDAP server for users")) + .onErrorMap(throwable -> new RuntimeException( + "Failed to check the LDAP server for users", + throwable)); + + Mono.when(checkAdminLdapServer, checkedUserLdapServer) + .block(DurationConst.ONE_MINUTE); + + clientBindTaskScheduler = new TaskScheduler(); + } + + @Override + public Mono verifyAndGrant(UserLoginInfo userLoginInfo) { + String password = userLoginInfo.password(); + if (StringUtil.isBlank(password)) { + return LOGIN_AUTHENTICATION_FAILED_MONO; + } + Long userId = userLoginInfo.userId(); + String filter = userSearchFilter.replace( + LdapIdentityAccessManagementUserProperties.SEARCH_FILTER_PLACEHOLDER_USER_ID, + userId.toString()); + return adminLdapClient.search(baseDn, + Scope.WHOLE_SUBTREE, + DerefAliases.ALWAYS, + // Use 2 so that we can refuse to authenticate user + // if there are more than 1 entry. + 2, + 0, + false, + SearchRequest.NO_ATTRIBUTES, + filter) + .flatMap(searchResult -> { + List entries = searchResult.getEntries(); + int size = entries.size(); + if (size == 0) { + return LOGGING_IN_USER_NOT_ACTIVE_MONO; + } + if (size > 1) { + return Mono.error( + ResponseException.get(ResponseStatusCode.SERVER_INTERNAL_ERROR, + "More than 1 entry found for the user (" + + userId + + "), " + + "which means that the filter \"" + + userSearchFilter + + "\" is wrong")); + } + String objectName = entries.getFirst() + .getObjectName(); + return authenticateUser(objectName, password) + .flatMap(authenticated -> authenticated + ? GRANTED_WITH_ALL_PERMISSIONS_MONO + : LOGIN_AUTHENTICATION_FAILED_MONO); + }); + } + + private Mono authenticateUser(String dn, String password) { + // RFC 4511: 4.2.1. Processing of the Bind Request + // After sending a BindRequest, clients MUST NOT send further LDAP PDUs + // until receiving the BindResponse. Similarly, servers SHOULD NOT + // process or respond to requests received while processing a + // BindRequest. + return clientBindTaskScheduler + .schedule(Mono.defer(() -> userLdapClient.bind(true, dn, password))); + } + +} \ No newline at end of file diff --git a/turms-gateway/src/main/java/im/turms/gateway/domain/session/service/NoopSessionIdentityAccessManager.java b/turms-gateway/src/main/java/im/turms/gateway/domain/session/service/NoopSessionIdentityAccessManager.java new file mode 100644 index 0000000000..75d59bbe4b --- /dev/null +++ b/turms-gateway/src/main/java/im/turms/gateway/domain/session/service/NoopSessionIdentityAccessManager.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2019 The Turms Project + * https://github.com/turms-im/turms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.turms.gateway.domain.session.service; + +import reactor.core.publisher.Mono; + +import im.turms.gateway.domain.session.bo.UserLoginInfo; +import im.turms.gateway.domain.session.bo.UserPermissionInfo; + +import static im.turms.gateway.domain.session.bo.UserPermissionInfo.GRANTED_WITH_ALL_PERMISSIONS_MONO; + +/** + * @author James Chen + */ +public class NoopSessionIdentityAccessManager implements SessionIdentityAccessManagementSupport { + + @Override + public Mono verifyAndGrant(UserLoginInfo userLoginInfo) { + return GRANTED_WITH_ALL_PERMISSIONS_MONO; + } +} \ No newline at end of file diff --git a/turms-gateway/src/main/java/im/turms/gateway/domain/session/service/PasswordSessionIdentityAccessManager.java b/turms-gateway/src/main/java/im/turms/gateway/domain/session/service/PasswordSessionIdentityAccessManager.java new file mode 100644 index 0000000000..fb1708080f --- /dev/null +++ b/turms-gateway/src/main/java/im/turms/gateway/domain/session/service/PasswordSessionIdentityAccessManager.java @@ -0,0 +1,80 @@ +/* + * Copyright (C) 2019 The Turms Project + * https://github.com/turms-im/turms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.turms.gateway.domain.session.service; + +import reactor.core.publisher.Mono; + +import im.turms.gateway.domain.session.bo.UserLoginInfo; +import im.turms.gateway.domain.session.bo.UserPermissionInfo; +import im.turms.server.common.infra.logging.core.logger.Logger; +import im.turms.server.common.infra.logging.core.logger.LoggerFactory; +import im.turms.server.common.infra.property.TurmsProperties; + +import static im.turms.gateway.domain.session.bo.UserPermissionInfo.GRANTED_WITH_ALL_PERMISSIONS; +import static im.turms.gateway.domain.session.bo.UserPermissionInfo.LOGGING_IN_USER_NOT_ACTIVE_MONO; +import static im.turms.gateway.domain.session.bo.UserPermissionInfo.LOGIN_AUTHENTICATION_FAILED; + +/** + * @author James Chen + */ +public class PasswordSessionIdentityAccessManager + implements SessionIdentityAccessManagementSupport { + + private static final Logger LOGGER = + LoggerFactory.getLogger(PasswordSessionIdentityAccessManager.class); + + private final UserService userService; + + public PasswordSessionIdentityAccessManager(UserService userService) { + this.userService = userService; + } + + @Override + public Mono verifyAndGrant(UserLoginInfo userLoginInfo) { + Long userId = userLoginInfo.userId(); + String password = userLoginInfo.password(); + return userService.isActiveAndNotDeleted(userId) + .flatMap(isActiveAndNotDeleted -> isActiveAndNotDeleted + ? userService.authenticate(userId, password) + .map(authenticated -> authenticated + ? GRANTED_WITH_ALL_PERMISSIONS + : LOGIN_AUTHENTICATION_FAILED) + : LOGGING_IN_USER_NOT_ACTIVE_MONO); + } + + @Override + public boolean updateGlobalProperties(TurmsProperties properties) { + boolean enableIdentityAccessManagement = properties.getGateway() + .getSession() + .getIdentityAccessManagement() + .isEnabled(); + if (enableIdentityAccessManagement && !userService.isEnabled()) { + // We refuse to update the wrong setting, otherwise users cannot log in + // until developers correct it, and it will be a big problem. + LOGGER.error( + "Refused an illegal operation that tried to enable the previously disabled password-based identity and access management, " + + "because " + + "\"turms.gateway.session.identity-access-management.enabled\" is false, or " + + "\"turms.gateway.session.identity-access-management.type\" is not \"password\" at startup. " + + "To enable it, you need to update the \"turms.gateway.session.identity-access-management.enabled\" setting to true and restart the server"); + return false; + } else { + return enableIdentityAccessManagement; + } + } +} \ No newline at end of file diff --git a/turms-gateway/src/main/java/im/turms/gateway/domain/session/service/SessionIdentityAccessManagementSupport.java b/turms-gateway/src/main/java/im/turms/gateway/domain/session/service/SessionIdentityAccessManagementSupport.java index 229155c133..78892a0ebc 100644 --- a/turms-gateway/src/main/java/im/turms/gateway/domain/session/service/SessionIdentityAccessManagementSupport.java +++ b/turms-gateway/src/main/java/im/turms/gateway/domain/session/service/SessionIdentityAccessManagementSupport.java @@ -17,17 +17,12 @@ package im.turms.gateway.domain.session.service; -import java.util.Map; -import jakarta.annotation.Nullable; -import jakarta.validation.constraints.NotNull; - import reactor.core.publisher.Mono; +import im.turms.gateway.domain.session.bo.UserLoginInfo; import im.turms.gateway.domain.session.bo.UserPermissionInfo; -import im.turms.server.common.access.client.dto.constant.DeviceType; -import im.turms.server.common.access.client.dto.constant.UserStatus; import im.turms.server.common.access.common.ResponseStatusCode; -import im.turms.server.common.domain.location.bo.Location; +import im.turms.server.common.infra.property.TurmsProperties; /** * @author James Chen @@ -39,14 +34,16 @@ public interface SessionIdentityAccessManagementSupport { * {@link ResponseStatusCode#LOGIN_AUTHENTICATION_FAILED}, * {@link ResponseStatusCode#LOGGING_IN_USER_NOT_ACTIVE} */ - Mono verifyAndGrant( - int version, - @NotNull Long userId, - @Nullable String password, - @NotNull DeviceType deviceType, - @Nullable Map deviceDetails, - @Nullable UserStatus userStatus, - @Nullable Location location, - @Nullable String ip); + Mono verifyAndGrant(UserLoginInfo userLoginInfo); + + /** + * @return whether enable the identity access management. + */ + default boolean updateGlobalProperties(TurmsProperties properties) { + return properties.getGateway() + .getSession() + .getIdentityAccessManagement() + .isEnabled(); + } -} +} \ No newline at end of file diff --git a/turms-gateway/src/main/java/im/turms/gateway/domain/session/service/SessionIdentityAccessManager.java b/turms-gateway/src/main/java/im/turms/gateway/domain/session/service/SessionIdentityAccessManager.java index 38e7c15928..b15d200b71 100644 --- a/turms-gateway/src/main/java/im/turms/gateway/domain/session/service/SessionIdentityAccessManager.java +++ b/turms-gateway/src/main/java/im/turms/gateway/domain/session/service/SessionIdentityAccessManager.java @@ -18,76 +18,43 @@ package im.turms.gateway.domain.session.service; import java.lang.reflect.Method; -import java.security.NoSuchAlgorithmException; -import java.time.Duration; -import java.util.Date; import java.util.Map; -import java.util.Set; import jakarta.annotation.Nullable; -import io.netty.handler.codec.http.HttpMethod; import reactor.core.publisher.Mono; -import reactor.netty.http.client.HttpClient; -import im.turms.gateway.access.client.common.authorization.policy.IllegalPolicyException; -import im.turms.gateway.access.client.common.authorization.policy.Policy; -import im.turms.gateway.access.client.common.authorization.policy.PolicyDeserializer; import im.turms.gateway.access.client.common.authorization.policy.PolicyManager; import im.turms.gateway.domain.session.bo.UserLoginInfo; import im.turms.gateway.domain.session.bo.UserPermissionInfo; import im.turms.gateway.infra.plugin.extension.UserAuthenticator; import im.turms.server.common.access.client.dto.constant.DeviceType; import im.turms.server.common.access.client.dto.constant.UserStatus; -import im.turms.server.common.access.client.dto.request.TurmsRequestTypePool; -import im.turms.server.common.access.common.ResponseStatusCode; import im.turms.server.common.domain.admin.constant.AdminConst; import im.turms.server.common.domain.location.bo.Location; -import im.turms.server.common.infra.collection.CollectionUtil; import im.turms.server.common.infra.exception.IncompatibleInternalChangeException; -import im.turms.server.common.infra.json.JsonUtil; -import im.turms.server.common.infra.lang.StringUtil; import im.turms.server.common.infra.logging.core.logger.Logger; import im.turms.server.common.infra.logging.core.logger.LoggerFactory; import im.turms.server.common.infra.plugin.PluginManager; import im.turms.server.common.infra.plugin.invoker.SequentialExtensionPointInvoker; import im.turms.server.common.infra.property.TurmsProperties; import im.turms.server.common.infra.property.TurmsPropertiesManager; -import im.turms.server.common.infra.property.constant.IdentityAccessManagementType; import im.turms.server.common.infra.property.env.gateway.identityaccessmanagement.IdentityAccessManagementProperties; -import im.turms.server.common.infra.property.env.gateway.identityaccessmanagement.http.HttpAuthenticationResponseExpectationProperties; -import im.turms.server.common.infra.property.env.gateway.identityaccessmanagement.http.HttpIdentityAccessManagementProperties; -import im.turms.server.common.infra.property.env.gateway.identityaccessmanagement.http.HttpIdentityAccessManagementRequestProperties; -import im.turms.server.common.infra.property.env.gateway.identityaccessmanagement.jwt.JwtAlgorithmProperties; -import im.turms.server.common.infra.property.env.gateway.identityaccessmanagement.jwt.JwtIdentityAccessManagementProperties; -import im.turms.server.common.infra.property.env.gateway.session.SessionProperties; -import im.turms.server.common.infra.security.jwt.Jwt; -import im.turms.server.common.infra.security.jwt.JwtManager; -import im.turms.server.common.infra.security.jwt.JwtPayload; -import im.turms.server.common.infra.security.jwt.exception.InvalidJwtException; -import im.turms.server.common.infra.security.jwt.exception.JwtSignatureVerificationException; -import im.turms.server.common.infra.validation.Validator; + +import static im.turms.gateway.domain.session.bo.UserPermissionInfo.GRANTED_WITH_ALL_PERMISSIONS; +import static im.turms.gateway.domain.session.bo.UserPermissionInfo.GRANTED_WITH_ALL_PERMISSIONS_MONO; +import static im.turms.gateway.domain.session.bo.UserPermissionInfo.LOGIN_AUTHENTICATION_FAILED; +import static im.turms.gateway.domain.session.bo.UserPermissionInfo.LOGIN_AUTHENTICATION_FAILED_MONO; /** * @author James Chen */ -public class SessionIdentityAccessManager implements SessionIdentityAccessManagementSupport { +public class SessionIdentityAccessManager { private static final Logger LOGGER = LoggerFactory.getLogger(SessionIdentityAccessManager.class); private static final Method AUTHENTICATE_METHOD; - private static final UserPermissionInfo GRANTED_WITH_ALL_PERMISSIONS = - new UserPermissionInfo(ResponseStatusCode.OK, TurmsRequestTypePool.ALL); - private static final Mono GRANTED_WITH_ALL_PERMISSIONS_MONO = - Mono.just(GRANTED_WITH_ALL_PERMISSIONS); - private static final UserPermissionInfo LOGIN_AUTHENTICATION_FAILED = - new UserPermissionInfo(ResponseStatusCode.LOGIN_AUTHENTICATION_FAILED); - private static final Mono LOGIN_AUTHENTICATION_FAILED_MONO = - Mono.just(LOGIN_AUTHENTICATION_FAILED); - private static final Mono LOGGING_IN_USER_NOT_ACTIVE_MONO = - Mono.just(new UserPermissionInfo(ResponseStatusCode.LOGGING_IN_USER_NOT_ACTIVE)); - static { try { AUTHENTICATE_METHOD = @@ -97,133 +64,47 @@ public class SessionIdentityAccessManager implements SessionIdentityAccessManage } } - private final JwtManager jwtManager; private final PluginManager pluginManager; - private final PolicyManager policyManager; - - private final UserService userService; - - private final HttpClient httpIdentityAccessManagementClient; - private final HttpMethod httpIdentityAccessManagementHttpMethod; - private final Set httpAuthenticationExpectedStatusCodes; - private final Map httpAuthenticationExpectedHeaders; - private final Map httpAuthenticationExpectedBodyFields; - - private final Map jwtAuthenticationExpectedCustomPayloadClaims; private boolean enableIdentityAccessManagement; - private final IdentityAccessManagementType identityAccessManagementType; + + private final SessionIdentityAccessManagementSupport sessionIdentityAccessManagementSupport; public SessionIdentityAccessManager( TurmsPropertiesManager propertiesManager, PluginManager pluginManager, UserService userService) { this.pluginManager = pluginManager; - this.userService = userService; - this.policyManager = new PolicyManager(); - updateGlobalProperties(propertiesManager.getGlobalProperties()); IdentityAccessManagementProperties identityAccessManagementProperties = propertiesManager.getLocalProperties() .getGateway() .getSession() .getIdentityAccessManagement(); - identityAccessManagementType = identityAccessManagementProperties.getType(); - JwtManager jwtManager = null; - Map jwtAuthenticationExpectedCustomPayloadClaims = null; - HttpClient httpIdentityAccessManagementClient = null; - HttpMethod httpIdentityAccessManagementHttpMethod = null; - Set httpAuthenticationExpectedStatusCodes = null; - Map httpAuthenticationExpectedHeaders = null; - Map httpAuthenticationExpectedBodyFields = null; - if (identityAccessManagementType == IdentityAccessManagementType.JWT) { - JwtIdentityAccessManagementProperties jwtProperties = - identityAccessManagementProperties.getJwt(); - jwtAuthenticationExpectedCustomPayloadClaims = jwtProperties.getAuthentication() - .getExpectation() - .getCustomPayloadClaims(); - JwtAlgorithmProperties jwtAlgorithmProperties = jwtProperties.getAlgorithm(); - jwtManager = new JwtManager( - jwtProperties.getVerification(), - jwtAlgorithmProperties.getRsa256(), - jwtAlgorithmProperties.getRsa384(), - jwtAlgorithmProperties.getRsa512(), - jwtAlgorithmProperties.getPs256(), - jwtAlgorithmProperties.getPs384(), - jwtAlgorithmProperties.getPs512(), - jwtAlgorithmProperties.getEcdsa256(), - jwtAlgorithmProperties.getEcdsa384(), - jwtAlgorithmProperties.getEcdsa512(), - jwtAlgorithmProperties.getHmac256(), - jwtAlgorithmProperties.getHmac384(), - jwtAlgorithmProperties.getHmac512()); - LOGGER.info("Supported algorithms for JWT: {}", - jwtManager.getSupportedAlgorithmNames()); - } else if (identityAccessManagementType == IdentityAccessManagementType.HTTP) { - HttpIdentityAccessManagementProperties httpProperties = - identityAccessManagementProperties.getHttp(); - HttpIdentityAccessManagementRequestProperties requestProperties = - httpProperties.getRequest(); - HttpAuthenticationResponseExpectationProperties responseExpectationProperties = - httpProperties.getAuthentication() - .getResponseExpectation(); - String url = requestProperties.getUrl(); - Exception exception = Validator.url(url); - if (exception != null) { - throw new IllegalArgumentException( - "Illegal HTTP URL: " - + url, - exception); - } - httpIdentityAccessManagementClient = HttpClient.create() - .baseUrl(url) - .headers(entries -> { - for (Map.Entry entry : requestProperties.getHeaders() - .entrySet()) { - entries.add(entry.getKey(), entry.getValue()); - } - }) - .responseTimeout(Duration.ofMillis(requestProperties.getTimeoutMillis())); - httpIdentityAccessManagementHttpMethod = - HttpMethod.valueOf(requestProperties.getHttpMethod() - .name()); - httpAuthenticationExpectedStatusCodes = responseExpectationProperties.getStatusCodes(); - httpAuthenticationExpectedHeaders = responseExpectationProperties.getHeaders(); - httpAuthenticationExpectedBodyFields = responseExpectationProperties.getBodyFields(); - } - this.jwtManager = jwtManager; - this.jwtAuthenticationExpectedCustomPayloadClaims = - jwtAuthenticationExpectedCustomPayloadClaims; - this.httpIdentityAccessManagementClient = httpIdentityAccessManagementClient; - this.httpIdentityAccessManagementHttpMethod = httpIdentityAccessManagementHttpMethod; - this.httpAuthenticationExpectedStatusCodes = httpAuthenticationExpectedStatusCodes; - this.httpAuthenticationExpectedHeaders = httpAuthenticationExpectedHeaders; - this.httpAuthenticationExpectedBodyFields = httpAuthenticationExpectedBodyFields; + + sessionIdentityAccessManagementSupport = + switch (identityAccessManagementProperties.getType()) { + case NOOP -> new NoopSessionIdentityAccessManager(); + case HTTP -> new HttpSessionIdentityAccessManager( + identityAccessManagementProperties.getHttp(), + new PolicyManager()); + case JWT -> new JwtSessionIdentityAccessManager( + identityAccessManagementProperties.getJwt(), + new PolicyManager()); + case PASSWORD -> new PasswordSessionIdentityAccessManager(userService); + case LDAP -> new LdapSessionIdentityAccessManager( + identityAccessManagementProperties.getLdap()); + }; + + updateGlobalProperties(propertiesManager.getGlobalProperties()); propertiesManager.addGlobalPropertiesChangeListener(this::updateGlobalProperties); } private void updateGlobalProperties(TurmsProperties properties) { - SessionProperties sessionProperties = properties.getGateway() - .getSession(); - boolean localEnableIdentityAccessManagement = - sessionProperties.getIdentityAccessManagement() - .isEnabled(); - if (localEnableIdentityAccessManagement - && userService == null - && identityAccessManagementType == IdentityAccessManagementType.PASSWORD) { - // We refuse to update the wrong setting, otherwise users cannot log in - // until developers correct it, and it will be a big problem. - LOGGER.error( - "Refused an illegal operation that tried to enable the disabled password-based identity and access management, " - + "because " - + "\"turms.gateway.session.identity-access-management.enabled\" is false, or " - + "\"turms.gateway.session.identity-access-management.type\" is not PASSWORD at startup. " - + "To enable it, you need to update the \"turms.gateway.session.identity-access-management.enabled\" setting to true and restart the server"); - } else { - enableIdentityAccessManagement = localEnableIdentityAccessManagement; - } + enableIdentityAccessManagement = + sessionIdentityAccessManagementSupport.updateGlobalProperties(properties); } - @Override + // @Override public Mono verifyAndGrant( int version, Long userId, @@ -249,14 +130,7 @@ public Mono verifyAndGrant( location, ip); Mono defaultVerifyAndGrantHandler = - Mono.defer(() -> switch (identityAccessManagementType) { - case NOOP -> GRANTED_WITH_ALL_PERMISSIONS_MONO; - case HTTP -> verifyAndGrantUsingHttp(userLoginInfo); - case JWT -> - verifyAndGrantUsingJwt(userLoginInfo.userId(), userLoginInfo.password()); - case PASSWORD -> verifyAndGrantUsingPassword(userLoginInfo.userId(), - userLoginInfo.password()); - }); + sessionIdentityAccessManagementSupport.verifyAndGrant(userLoginInfo); // TODO: Support authorization for plugins if (pluginManager.hasRunningExtensions(UserAuthenticator.class)) { Mono authenticate = @@ -275,99 +149,4 @@ public Mono verifyAndGrant( return defaultVerifyAndGrantHandler; } - private Mono verifyAndGrantUsingHttp(UserLoginInfo userLoginInfo) { - return httpIdentityAccessManagementClient.request(httpIdentityAccessManagementHttpMethod) - .send(Mono.fromCallable(() -> JsonUtil.write(userLoginInfo))) - .responseSingle((response, bodyBufferMono) -> { - if (!StringUtil.matchLatin1(response.status() - .toString(), httpAuthenticationExpectedStatusCodes)) { - return LOGIN_AUTHENTICATION_FAILED_MONO; - } - for (Map.Entry entry : httpAuthenticationExpectedHeaders - .entrySet()) { - if (!entry.getValue() - .equals(response.responseHeaders() - .get(entry.getKey()))) { - return LOGIN_AUTHENTICATION_FAILED_MONO; - } - } - return bodyBufferMono.asInputStream() - .map(inputStream -> { - Map map; - Policy policy; - try { - map = JsonUtil.readStringObjectMapValue(inputStream); - policy = PolicyDeserializer.parse(map); - } catch (Exception e) { - throw new IllegalArgumentException("Illegal request body", e); - } - if (!CollectionUtil.containsAllLooseComparison(map, - httpAuthenticationExpectedBodyFields)) { - return LOGIN_AUTHENTICATION_FAILED; - } - return new UserPermissionInfo( - ResponseStatusCode.OK, - policyManager.findAllowedRequestTypes(policy)); - }); - }); - } - - private Mono verifyAndGrantUsingJwt(Long userId, String jwtToken) { - if (StringUtil.isBlank(jwtToken)) { - return Mono.error( - new IllegalArgumentException("Invalid JWT token: JWT must not be blank")); - } - Jwt jwt; - try { - jwt = jwtManager.decode(jwtToken); - } catch (InvalidJwtException | NoSuchAlgorithmException - | JwtSignatureVerificationException e) { - return Mono.error(new IllegalArgumentException("Invalid JWT token", e)); - } - JwtPayload payload = jwt.payload(); - String subject = payload.subject(); - if (subject == null) { - return Mono.error(new IllegalArgumentException( - "Invalid JWT token: the sub claim in the payload must exist")); - } - if (!subject.equals(userId.toString())) { - return LOGIN_AUTHENTICATION_FAILED_MONO; - } - Date expiresAt = payload.expiresAt(); - Date notBefore = payload.notBefore(); - boolean hasExpiresAt = expiresAt != null; - boolean hasNotBefore = notBefore != null; - if (hasExpiresAt || hasNotBefore) { - long now = System.currentTimeMillis(); - if ((hasExpiresAt && expiresAt.getTime() <= now) - || (hasNotBefore && notBefore.getTime() > now)) { - return LOGIN_AUTHENTICATION_FAILED_MONO; - } - } - Map customClaims = payload.customClaims(); - if (!CollectionUtil.containsAllLooseComparison(customClaims, - jwtAuthenticationExpectedCustomPayloadClaims)) { - return LOGIN_AUTHENTICATION_FAILED_MONO; - } - Policy policy; - try { - policy = PolicyDeserializer.parse(customClaims); - } catch (IllegalPolicyException e) { - return Mono.error(new IllegalArgumentException("Invalid JWT token", e)); - } - return Mono.just(new UserPermissionInfo( - ResponseStatusCode.OK, - policyManager.findAllowedRequestTypes(policy))); - } - - private Mono verifyAndGrantUsingPassword(Long userId, String password) { - return userService.isActiveAndNotDeleted(userId) - .flatMap(isActiveAndNotDeleted -> isActiveAndNotDeleted - ? userService.authenticate(userId, password) - .map(authenticated -> authenticated - ? GRANTED_WITH_ALL_PERMISSIONS - : LOGIN_AUTHENTICATION_FAILED) - : LOGGING_IN_USER_NOT_ACTIVE_MONO); - } - } \ No newline at end of file diff --git a/turms-gateway/src/main/java/im/turms/gateway/domain/session/service/SessionService.java b/turms-gateway/src/main/java/im/turms/gateway/domain/session/service/SessionService.java index 5c43536af9..1e4122a7ed 100644 --- a/turms-gateway/src/main/java/im/turms/gateway/domain/session/service/SessionService.java +++ b/turms-gateway/src/main/java/im/turms/gateway/domain/session/service/SessionService.java @@ -39,7 +39,6 @@ import io.micrometer.core.instrument.Counter; import io.micrometer.core.instrument.MeterRegistry; import io.micrometer.core.instrument.Tags; -import lombok.experimental.Delegate; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; @@ -92,7 +91,7 @@ * @author James Chen */ @Service -public class SessionService implements ISessionService, SessionIdentityAccessManagementSupport { +public class SessionService implements ISessionService { private static final Logger LOGGER = LoggerFactory.getLogger(SessionService.class); @@ -103,7 +102,6 @@ public class SessionService implements ISessionService, SessionIdentityAccessMan private final HeartbeatManager heartbeatManager; private final PluginManager pluginManager; - @Delegate private final SessionIdentityAccessManager sessionAuthenticationManager; private final SessionLocationService sessionLocationService; @@ -242,14 +240,16 @@ public Mono handleLoginRequest( return Mono.error( ResponseException.get(ResponseStatusCode.LOGIN_FROM_FORBIDDEN_DEVICE_TYPE)); } - return verifyAndGrant(version, - userId, - password, - deviceType, - deviceDetails, - userStatus, - location, - ipStr).flatMap(permissionInfo -> { + return sessionAuthenticationManager + .verifyAndGrant(version, + userId, + password, + deviceType, + deviceDetails, + userStatus, + location, + ipStr) + .flatMap(permissionInfo -> { ResponseStatusCode statusCode = permissionInfo.authenticationCode(); return statusCode == ResponseStatusCode.OK ? tryRegisterOnlineUser(version, diff --git a/turms-gateway/src/main/java/im/turms/gateway/domain/session/service/UserService.java b/turms-gateway/src/main/java/im/turms/gateway/domain/session/service/UserService.java index 99b3bba75a..641f75a575 100644 --- a/turms-gateway/src/main/java/im/turms/gateway/domain/session/service/UserService.java +++ b/turms-gateway/src/main/java/im/turms/gateway/domain/session/service/UserService.java @@ -20,6 +20,7 @@ import jakarta.annotation.Nullable; import jakarta.validation.constraints.NotNull; +import lombok.Getter; import org.springframework.stereotype.Service; import reactor.core.publisher.Mono; @@ -36,10 +37,13 @@ public class UserService { private final UserRepository userRepository; private final PasswordManager passwordManager; + @Getter + private final boolean enabled; public UserService(UserRepository userRepository, PasswordManager passwordManager) { this.userRepository = userRepository; this.passwordManager = passwordManager; + enabled = userRepository.isEnabled(); } public Mono authenticate(@NotNull Long userId, @Nullable String rawPassword) { @@ -63,4 +67,4 @@ public Mono isActiveAndNotDeleted(@NotNull Long userId) { return userRepository.isActiveAndNotDeleted(userId); } -} +} \ No newline at end of file diff --git a/turms-gateway/src/main/java/im/turms/gateway/infra/ldap/asn1/BerBuffer.java b/turms-gateway/src/main/java/im/turms/gateway/infra/ldap/asn1/BerBuffer.java index 32e72ecba5..7c1420dfe2 100644 --- a/turms-gateway/src/main/java/im/turms/gateway/infra/ldap/asn1/BerBuffer.java +++ b/turms-gateway/src/main/java/im/turms/gateway/infra/ldap/asn1/BerBuffer.java @@ -17,6 +17,8 @@ package im.turms.gateway.infra.ldap.asn1; +import java.io.Closeable; +import java.io.IOException; import java.util.List; import io.netty.buffer.ByteBuf; @@ -31,7 +33,7 @@ /** * @author James Chen */ -public class BerBuffer implements ReferenceCounted { +public class BerBuffer implements Closeable, ReferenceCounted { private int[] sequenceLengthWriterIndexes; private int currentSequenceLengthIndex; @@ -466,6 +468,11 @@ public void skipBytes(int length) { buffer.skipBytes(length); } + @Override + public void close() throws IOException { + buffer.release(); + } + @Override public int refCnt() { return buffer.refCnt(); diff --git a/turms-gateway/src/test/java/unit/im/turms/gateway/domain/session/service/SessionServiceTests.java b/turms-gateway/src/test/java/unit/im/turms/gateway/domain/session/service/SessionServiceTests.java index 7143a2e0f0..0d95c8500c 100644 --- a/turms-gateway/src/test/java/unit/im/turms/gateway/domain/session/service/SessionServiceTests.java +++ b/turms-gateway/src/test/java/unit/im/turms/gateway/domain/session/service/SessionServiceTests.java @@ -281,6 +281,7 @@ private SessionService newSessionService( SessionLocationService locationService = mock(SessionLocationService.class); UserService userService = mock(UserService.class); + when(userService.isEnabled()).thenReturn(true); when(userService.isActiveAndNotDeleted(any())).thenReturn(Mono.just(isActiveAndNotDeleted)); when(userService.authenticate(any(), any())).thenReturn(Mono.just(isAuthenticated)); diff --git a/turms-gateway/src/test/java/unit/im/turms/gateway/infra/ldap/FilterTest.java b/turms-gateway/src/test/java/unit/im/turms/gateway/infra/ldap/FilterTest.java index b67b78edaf..59de61581a 100644 --- a/turms-gateway/src/test/java/unit/im/turms/gateway/infra/ldap/FilterTest.java +++ b/turms-gateway/src/test/java/unit/im/turms/gateway/infra/ldap/FilterTest.java @@ -114,9 +114,11 @@ private static void test(String filterString) { ASN1Element expectedElement = create(filterString).encode(); byte[] expectedBytes = expectedElement.encode(); - BerBuffer buffer = new BerBuffer(); - Filter.write(buffer, filterString); - byte[] actualBytes = buffer.getBytes(); + byte[] actualBytes; + try (BerBuffer buffer = new BerBuffer()) { + Filter.write(buffer, filterString); + actualBytes = buffer.getBytes(); + } // We encode the sequence in different ways, // so we need to normalize their representation. com.unboundid.ldap.sdk.Filter filter = decode(ASN1Element.decode(actualBytes)); diff --git a/turms-server-common/src/main/java/im/turms/server/common/infra/property/constant/IdentityAccessManagementType.java b/turms-server-common/src/main/java/im/turms/server/common/infra/property/constant/IdentityAccessManagementType.java index a47d3eca19..df0a693f35 100644 --- a/turms-server-common/src/main/java/im/turms/server/common/infra/property/constant/IdentityAccessManagementType.java +++ b/turms-server-common/src/main/java/im/turms/server/common/infra/property/constant/IdentityAccessManagementType.java @@ -24,5 +24,6 @@ public enum IdentityAccessManagementType { NOOP, HTTP, JWT, - PASSWORD + PASSWORD, + LDAP, } \ No newline at end of file diff --git a/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/gateway/identityaccessmanagement/IdentityAccessManagementProperties.java b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/gateway/identityaccessmanagement/IdentityAccessManagementProperties.java index 751417e908..aaa97f46d2 100644 --- a/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/gateway/identityaccessmanagement/IdentityAccessManagementProperties.java +++ b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/gateway/identityaccessmanagement/IdentityAccessManagementProperties.java @@ -26,6 +26,7 @@ import im.turms.server.common.infra.property.constant.IdentityAccessManagementType; import im.turms.server.common.infra.property.env.gateway.identityaccessmanagement.http.HttpIdentityAccessManagementProperties; import im.turms.server.common.infra.property.env.gateway.identityaccessmanagement.jwt.JwtIdentityAccessManagementProperties; +import im.turms.server.common.infra.property.env.gateway.identityaccessmanagement.ldap.LdapIdentityAccessManagementProperties; import im.turms.server.common.infra.property.metadata.Description; import im.turms.server.common.infra.property.metadata.GlobalProperty; import im.turms.server.common.infra.property.metadata.MutableProperty; @@ -53,6 +54,10 @@ public class IdentityAccessManagementProperties { @NestedConfigurationProperty private JwtIdentityAccessManagementProperties jwt = new JwtIdentityAccessManagementProperties(); + @NestedConfigurationProperty + private LdapIdentityAccessManagementProperties ldap = + new LdapIdentityAccessManagementProperties(); + @NestedConfigurationProperty private HttpIdentityAccessManagementProperties http = new HttpIdentityAccessManagementProperties(); diff --git a/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/gateway/identityaccessmanagement/ldap/LdapIdentityAccessManagementAdminProperties.java b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/gateway/identityaccessmanagement/ldap/LdapIdentityAccessManagementAdminProperties.java new file mode 100644 index 0000000000..01733ba9c9 --- /dev/null +++ b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/gateway/identityaccessmanagement/ldap/LdapIdentityAccessManagementAdminProperties.java @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2019 The Turms Project + * https://github.com/turms-im/turms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.turms.server.common.infra.property.env.gateway.identityaccessmanagement.ldap; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.boot.context.properties.NestedConfigurationProperty; + +import im.turms.server.common.infra.property.env.common.SslProperties; +import im.turms.server.common.infra.property.metadata.Description; +import im.turms.server.common.infra.security.SensitiveProperty; + +/** + * @author James Chen + */ +@AllArgsConstructor +@Builder(toBuilder = true) +@Data +@NoArgsConstructor +public class LdapIdentityAccessManagementAdminProperties { + + @Description("The host of LDAP server for admin") + private String host = "localhost"; + + @Description("The port of LDAP server for admin") + private int port = 389; + + @Description("The administrator's username for binding") + private String username = ""; + + @Description("The administrator's password for binding") + @SensitiveProperty + private String password = ""; + + @NestedConfigurationProperty + private transient SslProperties ssl = new SslProperties(); + +} \ No newline at end of file diff --git a/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/gateway/identityaccessmanagement/ldap/LdapIdentityAccessManagementProperties.java b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/gateway/identityaccessmanagement/ldap/LdapIdentityAccessManagementProperties.java new file mode 100644 index 0000000000..ee8a0571e2 --- /dev/null +++ b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/gateway/identityaccessmanagement/ldap/LdapIdentityAccessManagementProperties.java @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2019 The Turms Project + * https://github.com/turms-im/turms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.turms.server.common.infra.property.env.gateway.identityaccessmanagement.ldap; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.boot.context.properties.NestedConfigurationProperty; + +import im.turms.server.common.infra.property.metadata.Description; + +/** + * @author James Chen + */ +@AllArgsConstructor +@Builder(toBuilder = true) +@Data +@NoArgsConstructor +public class LdapIdentityAccessManagementProperties { + + @Description("The base DN from which all operations originate") + private String baseDn = ""; + + @NestedConfigurationProperty + private LdapIdentityAccessManagementAdminProperties admin = + new LdapIdentityAccessManagementAdminProperties(); + + @NestedConfigurationProperty + private LdapIdentityAccessManagementUserProperties user = + new LdapIdentityAccessManagementUserProperties(); + +} \ No newline at end of file diff --git a/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/gateway/identityaccessmanagement/ldap/LdapIdentityAccessManagementUserProperties.java b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/gateway/identityaccessmanagement/ldap/LdapIdentityAccessManagementUserProperties.java new file mode 100644 index 0000000000..b30c9ae146 --- /dev/null +++ b/turms-server-common/src/main/java/im/turms/server/common/infra/property/env/gateway/identityaccessmanagement/ldap/LdapIdentityAccessManagementUserProperties.java @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2019 The Turms Project + * https://github.com/turms-im/turms + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package im.turms.server.common.infra.property.env.gateway.identityaccessmanagement.ldap; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.boot.context.properties.NestedConfigurationProperty; + +import im.turms.server.common.infra.property.env.common.SslProperties; +import im.turms.server.common.infra.property.metadata.Description; +import im.turms.server.common.infra.security.SensitiveProperty; + +/** + * @author James Chen + */ +@AllArgsConstructor +@Builder(toBuilder = true) +@Data +@NoArgsConstructor +public class LdapIdentityAccessManagementUserProperties { + + public static final String SEARCH_FILTER_PLACEHOLDER_USER_ID = "${userId}"; + + @Description("The host of LDAP server for user") + private String host = "localhost"; + + @Description("The port of LDAP server for user") + private int port = 389; + + @Description("The search filter to find the user entry. " + + "\"" + + SEARCH_FILTER_PLACEHOLDER_USER_ID + + "\" is a placeholder and will be replaced with " + + "the user ID passed in the login request") + private String searchFilter = "uid=" + + SEARCH_FILTER_PLACEHOLDER_USER_ID; + + @NestedConfigurationProperty + private transient SslProperties ssl = new SslProperties(); + +} \ No newline at end of file diff --git a/turms-server-common/src/test/resources/turms-properties-metadata-with-property-value.json b/turms-server-common/src/test/resources/turms-properties-metadata-with-property-value.json index 37b67a33af..0a66d30866 100644 --- a/turms-server-common/src/test/resources/turms-properties-metadata-with-property-value.json +++ b/turms-server-common/src/test/resources/turms-properties-metadata-with-property-value.json @@ -1536,6 +1536,84 @@ } } }, + "ldap": { + "admin": { + "host": { + "deprecated": false, + "description": "The host of LDAP server for admin", + "global": false, + "mutable": false, + "sensitive": false, + "type": "string", + "value": "localhost" + }, + "password": { + "deprecated": false, + "description": "The administrator's password for binding", + "global": false, + "mutable": false, + "sensitive": true, + "type": "string", + "value": "" + }, + "port": { + "deprecated": false, + "description": "The port of LDAP server for admin", + "global": false, + "mutable": false, + "sensitive": false, + "type": "int", + "value": 389 + }, + "username": { + "deprecated": false, + "description": "The administrator's username for binding", + "global": false, + "mutable": false, + "sensitive": false, + "type": "string", + "value": "" + } + }, + "baseDn": { + "deprecated": false, + "description": "The base DN from which all operations originate", + "global": false, + "mutable": false, + "sensitive": false, + "type": "string", + "value": "" + }, + "user": { + "host": { + "deprecated": false, + "description": "The host of LDAP server for user", + "global": false, + "mutable": false, + "sensitive": false, + "type": "string", + "value": "localhost" + }, + "port": { + "deprecated": false, + "description": "The port of LDAP server for user", + "global": false, + "mutable": false, + "sensitive": false, + "type": "int", + "value": 389 + }, + "searchFilter": { + "deprecated": false, + "description": "The search filter to find the user entry. \"${userId}\" is a placeholder and will be replaced with the user ID passed in the login request", + "global": false, + "mutable": false, + "sensitive": false, + "type": "string", + "value": "uid=${userId}" + } + } + }, "type": { "deprecated": false, "description": "Note that if the type is not PASSWORD, turms-gateway will not connect to the MongoDB server for user records", @@ -1545,7 +1623,8 @@ "NOOP", "HTTP", "JWT", - "PASSWORD" + "PASSWORD", + "LDAP" ], "sensitive": false, "type": "enum", diff --git a/turms-server-common/src/test/resources/turms-properties-metadata.json b/turms-server-common/src/test/resources/turms-properties-metadata.json index 0862ebda95..d50f6d16a1 100644 --- a/turms-server-common/src/test/resources/turms-properties-metadata.json +++ b/turms-server-common/src/test/resources/turms-properties-metadata.json @@ -1375,6 +1375,76 @@ } } }, + "ldap": { + "admin": { + "host": { + "deprecated": false, + "description": "The host of LDAP server for admin", + "global": false, + "mutable": false, + "sensitive": false, + "type": "string" + }, + "password": { + "deprecated": false, + "description": "The administrator's password for binding", + "global": false, + "mutable": false, + "sensitive": true, + "type": "string" + }, + "port": { + "deprecated": false, + "description": "The port of LDAP server for admin", + "global": false, + "mutable": false, + "sensitive": false, + "type": "int" + }, + "username": { + "deprecated": false, + "description": "The administrator's username for binding", + "global": false, + "mutable": false, + "sensitive": false, + "type": "string" + } + }, + "baseDn": { + "deprecated": false, + "description": "The base DN from which all operations originate", + "global": false, + "mutable": false, + "sensitive": false, + "type": "string" + }, + "user": { + "host": { + "deprecated": false, + "description": "The host of LDAP server for user", + "global": false, + "mutable": false, + "sensitive": false, + "type": "string" + }, + "port": { + "deprecated": false, + "description": "The port of LDAP server for user", + "global": false, + "mutable": false, + "sensitive": false, + "type": "int" + }, + "searchFilter": { + "deprecated": false, + "description": "The search filter to find the user entry. \"${userId}\" is a placeholder and will be replaced with the user ID passed in the login request", + "global": false, + "mutable": false, + "sensitive": false, + "type": "string" + } + } + }, "type": { "deprecated": false, "description": "Note that if the type is not PASSWORD, turms-gateway will not connect to the MongoDB server for user records", @@ -1384,7 +1454,8 @@ "NOOP", "HTTP", "JWT", - "PASSWORD" + "PASSWORD", + "LDAP" ], "sensitive": false, "type": "enum" diff --git a/turms-server-common/src/test/resources/turms-properties-only-mutable-metadata.json b/turms-server-common/src/test/resources/turms-properties-only-mutable-metadata.json index 288837b3d2..e625e04b28 100644 --- a/turms-server-common/src/test/resources/turms-properties-only-mutable-metadata.json +++ b/turms-server-common/src/test/resources/turms-properties-only-mutable-metadata.json @@ -393,6 +393,10 @@ "expectation": {} }, "verification": {} + }, + "ldap": { + "admin": {}, + "user": {} } }, "minHeartbeatIntervalSeconds": {