Skip to content

Commit 30f6068

Browse files
committed
REST and gRPC chat room endpoints and service. AuthHeaderParser.scala
1 parent eaf6bec commit 30f6068

47 files changed

Lines changed: 7536 additions & 141 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

flushall_build_and_run.sh

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -149,19 +149,23 @@ JAVA_OPTS="--add-opens java.base/java.lang=ALL-UNNAMED \
149149
--add-opens java.base/java.util.jar=ALL-UNNAMED \
150150
--add-opens java.base/sun.reflect.generics.reflectiveObjects=ALL-UNNAMED"
151151

152+
RUNTIME_LOG=/tmp/obp-api.log
153+
152154
if [ "$RUN_BACKGROUND" = true ]; then
153-
# Run in background with output to log file
154-
nohup java $JAVA_OPTS -jar obp-api/target/obp-api.jar > http4s-server.log 2>&1 &
155+
# Run in background with output to log file (tee'd to /tmp as well)
156+
nohup java $JAVA_OPTS -jar obp-api/target/obp-api.jar > >(tee "$RUNTIME_LOG") 2>&1 &
155157
SERVER_PID=$!
156158
echo "✓ HTTP4S server started in background"
157159
echo " PID: $SERVER_PID"
158-
echo " Log: http4s-server.log"
160+
echo " Log: http4s-server.log (also $RUNTIME_LOG)"
159161
echo ""
160162
echo "To stop the server: kill $SERVER_PID"
161163
echo "To view logs: tail -f http4s-server.log"
162164
else
163-
# Run in foreground (Ctrl+C to stop)
165+
# Run in foreground (Ctrl+C to stop). Also tee output to /tmp so it can be
166+
# tailed from another terminal without taking over this one.
164167
echo "Press Ctrl+C to stop the server"
168+
echo "Runtime log also written to: $RUNTIME_LOG"
165169
echo ""
166-
java $JAVA_OPTS -jar obp-api/target/obp-api.jar
170+
java $JAVA_OPTS -jar obp-api/target/obp-api.jar 2>&1 | tee "$RUNTIME_LOG"
167171
fi

obp-api/pom.xml

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -323,16 +323,25 @@
323323
<artifactId>scalapb-runtime-grpc_${scala.version}</artifactId>
324324
<version>0.8.4</version>
325325
</dependency>
326-
<!-- https://mvnrepository.com/artifact/io.grpc/grpc-all -->
327326
<dependency>
328327
<groupId>io.grpc</groupId>
329-
<artifactId>grpc-all</artifactId>
328+
<artifactId>grpc-netty-shaded</artifactId>
330329
<version>1.48.1</version>
331330
</dependency>
332331
<dependency>
333-
<groupId>io.netty</groupId>
334-
<artifactId>netty-tcnative-boringssl-static</artifactId>
335-
<version>2.0.27.Final</version>
332+
<groupId>io.grpc</groupId>
333+
<artifactId>grpc-protobuf</artifactId>
334+
<version>1.48.1</version>
335+
</dependency>
336+
<dependency>
337+
<groupId>io.grpc</groupId>
338+
<artifactId>grpc-stub</artifactId>
339+
<version>1.48.1</version>
340+
</dependency>
341+
<dependency>
342+
<groupId>io.grpc</groupId>
343+
<artifactId>grpc-services</artifactId>
344+
<version>1.48.1</version>
336345
</dependency>
337346
<dependency>
338347
<groupId>org.asynchttpclient</groupId>
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
syntax = "proto3";
2+
package code.obp.grpc.chat.g1;
3+
4+
import "google/protobuf/timestamp.proto";
5+
6+
message StreamMessagesRequest {
7+
string chat_room_id = 1;
8+
}
9+
10+
// Fields match ChatMessageJsonV600 exactly, plus event_type for stream events
11+
message ChatMessageEvent {
12+
string event_type = 1;
13+
string chat_message_id = 2;
14+
string chat_room_id = 3;
15+
string sender_user_id = 4;
16+
string sender_consumer_id = 5;
17+
string sender_username = 6;
18+
string sender_provider = 7;
19+
string sender_consumer_name = 8;
20+
string content = 9;
21+
string message_type = 10;
22+
repeated string mentioned_user_ids = 11;
23+
string reply_to_message_id = 12;
24+
string thread_id = 13;
25+
bool is_deleted = 14;
26+
google.protobuf.Timestamp created_at = 15;
27+
google.protobuf.Timestamp updated_at = 16;
28+
}
29+
30+
message TypingEvent {
31+
string chat_room_id = 1;
32+
bool is_typing = 2;
33+
}
34+
35+
// Fields match TypingUserJsonV600
36+
message TypingIndicator {
37+
string chat_room_id = 1;
38+
string user_id = 2;
39+
string username = 3;
40+
string provider = 4;
41+
bool is_typing = 5;
42+
}
43+
44+
message StreamPresenceRequest {
45+
string chat_room_id = 1;
46+
}
47+
48+
message PresenceEvent {
49+
string user_id = 1;
50+
string username = 2;
51+
string provider = 3;
52+
bool is_online = 4;
53+
}
54+
55+
message StreamUnreadCountsRequest {
56+
}
57+
58+
message UnreadCountEvent {
59+
string chat_room_id = 1;
60+
int64 unread_count = 2;
61+
}
62+
63+
service ChatStreamService {
64+
rpc StreamMessages(StreamMessagesRequest) returns (stream ChatMessageEvent);
65+
rpc StreamTyping(stream TypingEvent) returns (stream TypingIndicator);
66+
rpc StreamPresence(StreamPresenceRequest) returns (stream PresenceEvent);
67+
rpc StreamUnreadCounts(StreamUnreadCountsRequest) returns (stream UnreadCountEvent);
68+
}

obp-api/src/main/resources/props/sample.props.template

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1196,10 +1196,13 @@ database_messages_scheduler_interval=3600
11961196
# GRPC
11971197
# the default GRPC is disabled
11981198
# grpc.server.enabled = false
1199-
# If do not set this props, the grpc port will be set randomly when OBP starts.
1200-
# And you can call `Get API Configuration` endpoint to see the `grpc_port` there.
1201-
# When you set this props, need to make sure this port is available.
1199+
# The default gRPC port is 50051. Override if needed.
12021200
# grpc.server.port = 50051
1201+
# When gRPC is enabled, chat streaming services (StreamMessages, StreamTyping,
1202+
# StreamPresence, StreamUnreadCounts) are available on the same port.
1203+
# Clients authenticate via the "authorization" metadata key using the same
1204+
# DirectLogin or OAuth tokens as the REST API.
1205+
# See src/main/protobuf/chat.proto for the service contract.
12031206

12041207
# Create System Views At Boot -----------------------------------------------
12051208
# In case is not defined default value is true

obp-api/src/main/scala/bootstrap/liftweb/Boot.scala

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ import code.migration.MigrationScriptLog
105105
import code.model._
106106
import code.model.dataAccess._
107107
import code.model.dataAccess.internalMapping.AccountIdMapping
108-
import code.obp.grpc.HelloWorldServer
108+
import code.obp.grpc.ObpGrpcServer
109109
import code.productAttributeattribute.MappedProductAttribute
110110
import code.productcollection.MappedProductCollection
111111
import code.productcollectionitem.MappedProductCollectionItem
@@ -131,7 +131,7 @@ import code.transaction_types.MappedTransactionType
131131
import code.transactionattribute.MappedTransactionAttribute
132132
import code.transactionrequests.{MappedTransactionRequest, MappedTransactionRequestTypeCharge, TransactionRequestReasons}
133133
import code.usercustomerlinks.MappedUserCustomerLink
134-
import code.customerlinks.MappedCustomerLink
134+
import code.customerlinks.CustomerLink
135135
import code.userlocks.UserLocks
136136
import code.users._
137137
import code.util.Helper.{MdcLoggable, ObpS, SILENCE_IS_GOLDEN}
@@ -762,6 +762,8 @@ class Boot extends MdcLoggable {
762762

763763
def schemifyAll() = {
764764
Schemifier.schemify(true, Schemifier.infoF _, ToSchemify.models: _*)
765+
// Create default system-level "general" chat room (all_users_are_participants = true)
766+
code.chat.ChatRoomTrait.chatRoomProvider.vend.getOrCreateDefaultRoom()
765767
}
766768

767769
private def showExceptionAtJson(error: Throwable): String = {
@@ -1162,7 +1164,7 @@ object ToSchemify {
11621164
MappedNarrative,
11631165
MappedCustomer,
11641166
MappedUserCustomerLink,
1165-
MappedCustomerLink,
1167+
CustomerLink,
11661168
Consumer,
11671169
Token,
11681170
OpenIDConnectToken,
@@ -1205,12 +1207,16 @@ object ToSchemify {
12051207
CounterpartyAttributeMapper,
12061208
BankAccountBalance,
12071209
Group,
1208-
AccountAccessRequest
1210+
AccountAccessRequest,
1211+
code.chat.ChatRoom,
1212+
code.chat.Participant,
1213+
code.chat.ChatMessage,
1214+
code.chat.Reaction
12091215
)
12101216

12111217
// start grpc server
12121218
if (APIUtil.getPropsAsBoolValue("grpc.server.enabled", false)) {
1213-
val server = new HelloWorldServer(ExecutionContext.global)
1219+
val server = new ObpGrpcServer(ExecutionContext.global)
12141220
server.start()
12151221
LiftRules.unloadHooks.append(server.stop)
12161222
}

obp-api/src/main/scala/code/api/util/ApiRole.scala

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1370,6 +1370,20 @@ object ApiRole extends MdcLoggable{
13701370
case class CanGetAccountDirectoryAtOneBank(requiresBankId: Boolean = true) extends ApiRole
13711371
lazy val canGetAccountDirectoryAtOneBank = CanGetAccountDirectoryAtOneBank()
13721372

1373+
// Chat Room roles
1374+
case class CanDeleteBankChatRoom(requiresBankId: Boolean = true) extends ApiRole
1375+
lazy val canDeleteBankChatRoom = CanDeleteBankChatRoom()
1376+
case class CanDeleteSystemChatRoom(requiresBankId: Boolean = false) extends ApiRole
1377+
lazy val canDeleteSystemChatRoom = CanDeleteSystemChatRoom()
1378+
case class CanArchiveBankChatRoom(requiresBankId: Boolean = true) extends ApiRole
1379+
lazy val canArchiveBankChatRoom = CanArchiveBankChatRoom()
1380+
case class CanArchiveSystemChatRoom(requiresBankId: Boolean = false) extends ApiRole
1381+
lazy val canArchiveSystemChatRoom = CanArchiveSystemChatRoom()
1382+
case class CanSetBankChatRoomAUAP(requiresBankId: Boolean = true) extends ApiRole
1383+
lazy val canSetBankChatRoomAUAP = CanSetBankChatRoomAUAP()
1384+
case class CanSetSystemChatRoomAUAP(requiresBankId: Boolean = false) extends ApiRole
1385+
lazy val canSetSystemChatRoomAUAP = CanSetSystemChatRoomAUAP()
1386+
13731387
private val dynamicApiRoles = new ConcurrentHashMap[String, ApiRole]
13741388

13751389
private case class DynamicApiRole(role: String, requiresBankId: Boolean = false) extends ApiRole{

obp-api/src/main/scala/code/api/util/ApiTag.scala

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ object ApiTag {
8989
val apiTagAggregateMetrics = ResourceDocTag("Aggregate-Metrics")
9090
val apiTagSystemIntegrity = ResourceDocTag("System-Integrity")
9191
val apiTagBalance = ResourceDocTag("Balance")
92+
val apiTagChat = ResourceDocTag("Chat")
9293
val apiTagGroup = ResourceDocTag("Group")
9394
val apiTagWebhook = ResourceDocTag("Webhook")
9495
val apiTagMockedData = ResourceDocTag("Mocked-Data")
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package code.api.util
2+
3+
import net.liftweb.common.{Box, Empty, Full}
4+
5+
/**
6+
* Transport-independent parsing of the HTTP `Authorization` header value
7+
* into the subset of CallContext fields used by the authentication chain.
8+
*
9+
* The auth chain in [[APIUtil.getUserAndSessionContextFuture]] identifies which
10+
* scheme to use (OAuth 2, OAuth 1.0a, DirectLogin, Gateway Login, DAuth) by
11+
* reading CallContext.authReqHeaderField / directLoginParams / oAuthParams —
12+
* *not* requestHeaders. Every transport that supports authentication (REST
13+
* via http4s, gRPC, etc.) must populate these three fields identically,
14+
* otherwise schemes will silently fail to match and the chain will fall through
15+
* to "OBP-20080 Authorization Header format is not supported".
16+
*
17+
* This helper is the single source of truth for that parsing so that all
18+
* transports stay in sync.
19+
*/
20+
object AuthHeaderParser {
21+
22+
/** Result of parsing an Authorization header value. */
23+
final case class ParsedAuthHeader(
24+
authReqHeaderField: Box[String],
25+
directLoginParams: Map[String, String],
26+
oAuthParams: Map[String, String]
27+
)
28+
29+
private val EmptyParsed: ParsedAuthHeader =
30+
ParsedAuthHeader(Empty, Map.empty, Map.empty)
31+
32+
private val DirectLoginAllowedParameters: List[String] =
33+
List("consumer_key", "token", "username", "password")
34+
35+
/**
36+
* Parse an Authorization header value (e.g. "Bearer eyJ...", "DirectLogin token=...",
37+
* 'OAuth oauth_consumer_key="..."') into the auth-related CallContext fields.
38+
*
39+
* Returns empty fields when no header value is present.
40+
*/
41+
def parseAuthorizationHeader(authHeaderValue: Option[String]): ParsedAuthHeader =
42+
authHeaderValue match {
43+
case None => EmptyParsed
44+
case Some(value) =>
45+
ParsedAuthHeader(
46+
authReqHeaderField = Full(value),
47+
directLoginParams = if (value.contains("DirectLogin")) parseDirectLoginHeader(value) else Map.empty,
48+
oAuthParams = if (value.startsWith("OAuth ")) parseOAuthHeader(value) else Map.empty
49+
)
50+
}
51+
52+
/**
53+
* Parse a DirectLogin header value into its named parameters.
54+
* Accepts both:
55+
* - `DirectLogin token="xxx", username="yyy"` (old Authorization header format, with prefix)
56+
* - `token="xxx", username="yyy"` (new dedicated `DirectLogin:` header, no prefix)
57+
*
58+
* Only the whitelisted parameters (`consumer_key`, `token`, `username`, `password`)
59+
* are kept. Mirrors Lift's getAllParameters in directlogin.scala.
60+
*/
61+
def parseDirectLoginHeader(headerValue: String): Map[String, String] = {
62+
val cleanedParameterList = headerValue.stripPrefix("DirectLogin").split(",").map(_.trim).toList
63+
cleanedParameterList.flatMap { input =>
64+
if (input.contains("=")) {
65+
val split = input.split("=", 2)
66+
val paramName = split(0).trim
67+
val paramValue = split(1).replaceAll("^\"|\"$", "").trim
68+
if (DirectLoginAllowedParameters.contains(paramName) && paramValue.nonEmpty)
69+
Some(paramName -> paramValue)
70+
else
71+
None
72+
} else {
73+
None
74+
}
75+
}.toMap
76+
}
77+
78+
/**
79+
* Parse an OAuth 1.0a Authorization header value into its named parameters.
80+
* Format: `OAuth oauth_consumer_key="xxx", oauth_token="yyy", ...`
81+
*/
82+
def parseOAuthHeader(headerValue: String): Map[String, String] = {
83+
val oauthPart = headerValue.stripPrefix("OAuth ").trim
84+
val pattern = """(\w+)="([^"]*)"""".r
85+
pattern.findAllMatchIn(oauthPart).map(m => m.group(1) -> m.group(2)).toMap
86+
}
87+
}

obp-api/src/main/scala/code/api/util/ErrorMessages.scala

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -697,6 +697,22 @@ object ErrorMessages {
697697
val AbacRuleTooPermissive = "OBP-38010: ABAC rule is too permissive. The rule code contains a tautological expression (e.g. 'true', '1==1') that would always grant access. Please write a rule that checks specific attributes."
698698
val AbacRuleStatisticallyTooPermissive = "OBP-38011: ABAC rule is statistically too permissive. When evaluated against a sample of system users with no resource context, the rule grants access to more than 50% of users. Please write a more selective rule that checks specific attributes."
699699

700+
// Chat / Messaging related messages (OBP-39XXX)
701+
val ChatRoomNotFound = "OBP-39001: Chat Room not found. Please specify a valid value for CHAT_ROOM_ID."
702+
val ChatRoomAlreadyExists = "OBP-39002: Chat Room with this name already exists in this bank."
703+
val ChatRoomIsArchived = "OBP-39003: Chat Room is archived. No new messages or participants can be added."
704+
val NotChatRoomParticipant = "OBP-39004: Current user is not a participant of this Chat Room."
705+
val ChatMessageNotFound = "OBP-39005: Chat Message not found. Please specify a valid value for CHAT_MESSAGE_ID."
706+
val ChatRoomParticipantAlreadyExists = "OBP-39006: User is already a participant of this Chat Room."
707+
val ChatRoomParticipantNotFound = "OBP-39007: Participant not found in this Chat Room."
708+
val InsufficientChatPermission = "OBP-39008: You do not have the required permission for this Chat Room action."
709+
val CannotEditOthersMessage = "OBP-39009: You can only edit your own messages."
710+
val CannotDeleteMessage = "OBP-39010: You do not have permission to delete this message."
711+
val InvalidJoiningKey = "OBP-39011: Invalid joining key. The key may have been refreshed."
712+
val ReactionAlreadyExists = "OBP-39012: You have already added this reaction to this message."
713+
val ReactionNotFound = "OBP-39013: Reaction not found."
714+
val MustSpecifyUserIdOrConsumerId = "OBP-39014: Must specify either user_id or consumer_id, but not both."
715+
700716
// Transaction Request related messages (OBP-40XXX)
701717
val InvalidTransactionRequestType = "OBP-40001: Invalid value for TRANSACTION_REQUEST_TYPE"
702718
val InsufficientAuthorisationToCreateTransactionRequest = "OBP-40002: Insufficient authorisation to create TransactionRequest. " +

0 commit comments

Comments
 (0)