|  | 
| 15 | 15 | package iam | 
| 16 | 16 | 
 | 
| 17 | 17 | import ( | 
|  | 18 | +	"fmt" | 
| 18 | 19 | 	"net/http" | 
| 19 | 20 | 	"net/url" | 
| 20 | 21 | 	"os" | 
| 21 | 22 | 	"testing" | 
|  | 23 | +	"time" | 
| 22 | 24 | 
 | 
| 23 | 25 | 	"github.com/AccelByte/go-restful-plugins/v4/pkg/constant" | 
| 24 | 26 | 	"github.com/AccelByte/iam-go-sdk/v2" | 
| @@ -1067,3 +1069,265 @@ func TestFilterInitializationOptionsFromEnv_SubdomainValidationExcludedNamespace | 
| 1067 | 1069 | 	options = FilterInitializationOptionsFromEnv() | 
| 1068 | 1070 | 	assert.Empty(t, options.SubdomainValidationExcludedNamespaces) | 
| 1069 | 1071 | } | 
|  | 1072 | + | 
|  | 1073 | +func TestWithoutBannedTopics(t *testing.T) { | 
|  | 1074 | +	timeNow := time.Now().UTC() | 
|  | 1075 | +	futureBanTime := timeNow.Add(24 * time.Hour) | 
|  | 1076 | +	pastBanTime := timeNow.Add(-24 * time.Hour) | 
|  | 1077 | +	gameNamespace, publisherNamespace := "game", "publisher" | 
|  | 1078 | + | 
|  | 1079 | +	testCases := []struct { | 
|  | 1080 | +		name         string | 
|  | 1081 | +		bannedTopics []string | 
|  | 1082 | +		claims       *iam.JWTClaims | 
|  | 1083 | +		wantErr      bool | 
|  | 1084 | +		errMessage   restful.ServiceError | 
|  | 1085 | +	}{ | 
|  | 1086 | +		{ | 
|  | 1087 | +			name:         "nil claims should pass", | 
|  | 1088 | +			bannedTopics: []string{ChatBanTopic, MatchmakingBanTopic}, | 
|  | 1089 | +			claims:       nil, | 
|  | 1090 | +			wantErr:      false, | 
|  | 1091 | +		}, | 
|  | 1092 | +		{ | 
|  | 1093 | +			name:         "empty banned topics should pass", | 
|  | 1094 | +			bannedTopics: []string{}, | 
|  | 1095 | +			claims: &iam.JWTClaims{ | 
|  | 1096 | +				Namespace:      gameNamespace, | 
|  | 1097 | +				UnionNamespace: publisherNamespace, | 
|  | 1098 | +				Bans: []iam.JWTBan{ | 
|  | 1099 | +					{Ban: ChatBanTopic, EndDate: futureBanTime, TargetedNamespace: gameNamespace}, | 
|  | 1100 | +				}, | 
|  | 1101 | +			}, | 
|  | 1102 | +			wantErr: false, | 
|  | 1103 | +		}, | 
|  | 1104 | +		{ | 
|  | 1105 | +			name:         "non-matching ban topic should pass", | 
|  | 1106 | +			bannedTopics: []string{MatchmakingBanTopic}, | 
|  | 1107 | +			claims: &iam.JWTClaims{ | 
|  | 1108 | +				Namespace:      gameNamespace, | 
|  | 1109 | +				UnionNamespace: publisherNamespace, | 
|  | 1110 | +				Bans: []iam.JWTBan{ | 
|  | 1111 | +					{Ban: ChatBanTopic, EndDate: futureBanTime, TargetedNamespace: gameNamespace}, | 
|  | 1112 | +				}, | 
|  | 1113 | +			}, | 
|  | 1114 | +			wantErr: false, | 
|  | 1115 | +		}, | 
|  | 1116 | +		{ | 
|  | 1117 | +			name:         "expired ban should pass", | 
|  | 1118 | +			bannedTopics: []string{ChatBanTopic}, | 
|  | 1119 | +			claims: &iam.JWTClaims{ | 
|  | 1120 | +				Namespace:      gameNamespace, | 
|  | 1121 | +				UnionNamespace: publisherNamespace, | 
|  | 1122 | +				Bans: []iam.JWTBan{ | 
|  | 1123 | +					{Ban: ChatBanTopic, EndDate: pastBanTime, TargetedNamespace: gameNamespace}, | 
|  | 1124 | +				}, | 
|  | 1125 | +			}, | 
|  | 1126 | +			wantErr: false, | 
|  | 1127 | +		}, | 
|  | 1128 | +		{ | 
|  | 1129 | +			name:         "active matching ban should fail", | 
|  | 1130 | +			bannedTopics: []string{MatchmakingBanTopic}, | 
|  | 1131 | +			claims: &iam.JWTClaims{ | 
|  | 1132 | +				Namespace:      gameNamespace, | 
|  | 1133 | +				UnionNamespace: publisherNamespace, | 
|  | 1134 | +				Bans: []iam.JWTBan{ | 
|  | 1135 | +					{Ban: MatchmakingBanTopic, EndDate: futureBanTime, TargetedNamespace: gameNamespace}, | 
|  | 1136 | +				}, | 
|  | 1137 | +			}, | 
|  | 1138 | +			wantErr: true, | 
|  | 1139 | +			errMessage: respondError(http.StatusForbidden, ForbiddenAccess, | 
|  | 1140 | +				fmt.Sprintf("access forbidden: user is banned due to %s ban until %s", MatchmakingBanTopic, futureBanTime.Format(time.RFC3339))), | 
|  | 1141 | +		}, | 
|  | 1142 | +		{ | 
|  | 1143 | +			name:         "multiple bans with one active matching should fail", | 
|  | 1144 | +			bannedTopics: []string{ChatBanTopic}, | 
|  | 1145 | +			claims: &iam.JWTClaims{ | 
|  | 1146 | +				Namespace:      gameNamespace, | 
|  | 1147 | +				UnionNamespace: publisherNamespace, | 
|  | 1148 | +				Bans: []iam.JWTBan{ | 
|  | 1149 | +					{Ban: "OTHER", EndDate: futureBanTime, TargetedNamespace: gameNamespace}, | 
|  | 1150 | +					{Ban: ChatBanTopic, EndDate: futureBanTime, TargetedNamespace: gameNamespace}, | 
|  | 1151 | +				}, | 
|  | 1152 | +			}, | 
|  | 1153 | +			wantErr: true, | 
|  | 1154 | +			errMessage: respondError(http.StatusForbidden, ForbiddenAccess, | 
|  | 1155 | +				fmt.Sprintf("access forbidden: user is banned due to %s ban until %s", ChatBanTopic, futureBanTime.Format(time.RFC3339))), | 
|  | 1156 | +		}, | 
|  | 1157 | +		{ | 
|  | 1158 | +			name:         "active ban present, but bannedTopics does not match ban, should success", | 
|  | 1159 | +			bannedTopics: []string{ChatBanTopic}, | 
|  | 1160 | +			claims: &iam.JWTClaims{ | 
|  | 1161 | +				Namespace:      gameNamespace, | 
|  | 1162 | +				UnionNamespace: publisherNamespace, | 
|  | 1163 | +				Bans: []iam.JWTBan{ | 
|  | 1164 | +					{Ban: MatchmakingBanTopic, EndDate: futureBanTime, TargetedNamespace: gameNamespace}, | 
|  | 1165 | +				}, | 
|  | 1166 | +			}, | 
|  | 1167 | +			wantErr: false, | 
|  | 1168 | +		}, | 
|  | 1169 | +		{ | 
|  | 1170 | +			name:         "active ban present, but bannedTopics is empty, should succeed", | 
|  | 1171 | +			bannedTopics: []string{""}, | 
|  | 1172 | +			claims: &iam.JWTClaims{ | 
|  | 1173 | +				Namespace:      gameNamespace, | 
|  | 1174 | +				UnionNamespace: publisherNamespace, | 
|  | 1175 | +				Bans: []iam.JWTBan{ | 
|  | 1176 | +					{Ban: MatchmakingBanTopic, EndDate: futureBanTime, TargetedNamespace: gameNamespace}, | 
|  | 1177 | +				}, | 
|  | 1178 | +			}, | 
|  | 1179 | +			wantErr: false, | 
|  | 1180 | +		}, | 
|  | 1181 | +	} | 
|  | 1182 | + | 
|  | 1183 | +	for _, tc := range testCases { | 
|  | 1184 | +		t.Run(tc.name, func(t *testing.T) { | 
|  | 1185 | +			filterOpt := WithoutBannedTopics(tc.bannedTopics) | 
|  | 1186 | +			err := filterOpt(&restful.Request{}, nil, tc.claims) | 
|  | 1187 | + | 
|  | 1188 | +			if tc.wantErr { | 
|  | 1189 | +				assert.Error(t, err) | 
|  | 1190 | +				svcErr, ok := err.(restful.ServiceError) | 
|  | 1191 | +				assert.True(t, ok) | 
|  | 1192 | +				assert.Equal(t, http.StatusForbidden, svcErr.Code) | 
|  | 1193 | +				assert.Equal(t, tc.errMessage.Message, svcErr.Message) | 
|  | 1194 | +			} else { | 
|  | 1195 | +				assert.NoError(t, err) | 
|  | 1196 | +			} | 
|  | 1197 | +		}) | 
|  | 1198 | +	} | 
|  | 1199 | +} | 
|  | 1200 | + | 
|  | 1201 | +// nolint:paralleltest | 
|  | 1202 | +func TestWithoutBannedTopics_TargetedNamespaceAndExpiry(t *testing.T) { | 
|  | 1203 | +	now := time.Now().UTC() | 
|  | 1204 | +	future := now.Add(24 * time.Hour) | 
|  | 1205 | +	// EndDate equal to now (not in future) | 
|  | 1206 | +	nowEnd := now | 
|  | 1207 | + | 
|  | 1208 | +	testCases := []struct { | 
|  | 1209 | +		name       string | 
|  | 1210 | +		claims     *iam.JWTClaims | 
|  | 1211 | +		banned     []string | 
|  | 1212 | +		wantErr    bool | 
|  | 1213 | +		wantErrMsg string | 
|  | 1214 | +	}{ | 
|  | 1215 | +		{ | 
|  | 1216 | +			name: "ban targets studio namespace -> allow chat on game namespace", | 
|  | 1217 | +			claims: &iam.JWTClaims{ | 
|  | 1218 | +				Namespace:      "game", | 
|  | 1219 | +				UnionNamespace: "publisher", | 
|  | 1220 | +				Bans: []iam.JWTBan{ | 
|  | 1221 | +					{Ban: ChatBanTopic, TargetedNamespace: "publisher", EndDate: future}, | 
|  | 1222 | +				}, | 
|  | 1223 | +			}, | 
|  | 1224 | +			banned:  []string{ChatBanTopic}, | 
|  | 1225 | +			wantErr: false, | 
|  | 1226 | +		}, | 
|  | 1227 | +		{ | 
|  | 1228 | +			name: "ban targets different namespace -> allow", | 
|  | 1229 | +			claims: &iam.JWTClaims{ | 
|  | 1230 | +				Namespace:      "publisher", | 
|  | 1231 | +				UnionNamespace: "publisher", | 
|  | 1232 | +				Bans: []iam.JWTBan{ | 
|  | 1233 | +					{Ban: ChatBanTopic, TargetedNamespace: "game", EndDate: future}, | 
|  | 1234 | +				}, | 
|  | 1235 | +			}, | 
|  | 1236 | +			banned:  []string{ChatBanTopic}, | 
|  | 1237 | +			wantErr: false, | 
|  | 1238 | +		}, | 
|  | 1239 | +		{ | 
|  | 1240 | +			name: "ban targets same namespace -> forbid", | 
|  | 1241 | +			claims: &iam.JWTClaims{ | 
|  | 1242 | +				Namespace: "game", | 
|  | 1243 | +				Bans: []iam.JWTBan{ | 
|  | 1244 | +					{Ban: ChatBanTopic, TargetedNamespace: "game", EndDate: future}, | 
|  | 1245 | +				}, | 
|  | 1246 | +			}, | 
|  | 1247 | +			banned:  []string{ChatBanTopic}, | 
|  | 1248 | +			wantErr: true, | 
|  | 1249 | +		}, | 
|  | 1250 | +		{ | 
|  | 1251 | +			name: "targeted namespace case-insensitive match -> forbid", | 
|  | 1252 | +			claims: &iam.JWTClaims{ | 
|  | 1253 | +				Namespace: "game", | 
|  | 1254 | +				Bans: []iam.JWTBan{ | 
|  | 1255 | +					{Ban: MatchmakingBanTopic, TargetedNamespace: "GAME", EndDate: future}, | 
|  | 1256 | +				}, | 
|  | 1257 | +			}, | 
|  | 1258 | +			banned:  []string{MatchmakingBanTopic}, | 
|  | 1259 | +			wantErr: true, | 
|  | 1260 | +		}, | 
|  | 1261 | +		{ | 
|  | 1262 | +			name: "ban end date equal to now -> allow (not before)", | 
|  | 1263 | +			claims: &iam.JWTClaims{ | 
|  | 1264 | +				Namespace: "game", | 
|  | 1265 | +				Bans: []iam.JWTBan{ | 
|  | 1266 | +					{Ban: ChatBanTopic, TargetedNamespace: "game", EndDate: nowEnd}, | 
|  | 1267 | +				}, | 
|  | 1268 | +			}, | 
|  | 1269 | +			banned:  []string{ChatBanTopic}, | 
|  | 1270 | +			wantErr: false, | 
|  | 1271 | +		}, | 
|  | 1272 | +		{ | 
|  | 1273 | +			name: "ban end date equal to now -> allow (not before)", | 
|  | 1274 | +			claims: &iam.JWTClaims{ | 
|  | 1275 | +				Namespace: "game", | 
|  | 1276 | +				Bans: []iam.JWTBan{ | 
|  | 1277 | +					{Ban: ChatBanTopic, TargetedNamespace: "game", EndDate: nowEnd}, | 
|  | 1278 | +				}, | 
|  | 1279 | +			}, | 
|  | 1280 | +			banned:  []string{ChatBanTopic}, | 
|  | 1281 | +			wantErr: false, | 
|  | 1282 | +		}, | 
|  | 1283 | +	} | 
|  | 1284 | + | 
|  | 1285 | +	for _, tc := range testCases { | 
|  | 1286 | +		t.Run(tc.name, func(t *testing.T) { | 
|  | 1287 | +			opt := WithoutBannedTopics(tc.banned) | 
|  | 1288 | +			err := opt(&restful.Request{}, nil, tc.claims) | 
|  | 1289 | +			if tc.wantErr { | 
|  | 1290 | +				assert.Error(t, err) | 
|  | 1291 | +				svcErr, ok := err.(restful.ServiceError) | 
|  | 1292 | +				assert.True(t, ok) | 
|  | 1293 | +				assert.Equal(t, http.StatusForbidden, svcErr.Code) | 
|  | 1294 | +				// message should match respondError output for ForbiddenAccess | 
|  | 1295 | +				expected := respondError(http.StatusForbidden, ForbiddenAccess, | 
|  | 1296 | +					fmt.Sprintf("access forbidden: user is banned due to %s ban until %s", tc.claims.Bans[0].Ban, tc.claims.Bans[0].EndDate.Format(time.RFC3339))) | 
|  | 1297 | +				assert.Equal(t, expected.Message, svcErr.Message) | 
|  | 1298 | +			} else { | 
|  | 1299 | +				assert.NoError(t, err) | 
|  | 1300 | +			} | 
|  | 1301 | +		}) | 
|  | 1302 | +	} | 
|  | 1303 | +} | 
|  | 1304 | + | 
|  | 1305 | +// nolint:paralleltest | 
|  | 1306 | +func TestWithoutBannedTopics_BannedTopicCaseSensitivity(t *testing.T) { | 
|  | 1307 | +	now := time.Now().UTC().Add(24 * time.Hour) | 
|  | 1308 | + | 
|  | 1309 | +	claims := &iam.JWTClaims{ | 
|  | 1310 | +		Namespace: "game", | 
|  | 1311 | +		Bans: []iam.JWTBan{ | 
|  | 1312 | +			{Ban: ChatBanTopic, TargetedNamespace: "game", EndDate: now}, | 
|  | 1313 | +		}, | 
|  | 1314 | +	} | 
|  | 1315 | + | 
|  | 1316 | +	t.Run("bannedTopics lower-case should match uppercase ban", func(t *testing.T) { | 
|  | 1317 | +		opt := WithoutBannedTopics([]string{"chat"}) | 
|  | 1318 | +		err := opt(&restful.Request{}, nil, claims) | 
|  | 1319 | +		assert.Error(t, err) | 
|  | 1320 | +		svcErr, ok := err.(restful.ServiceError) | 
|  | 1321 | +		assert.True(t, ok) | 
|  | 1322 | +		assert.Equal(t, http.StatusForbidden, svcErr.Code) | 
|  | 1323 | +	}) | 
|  | 1324 | + | 
|  | 1325 | +	t.Run("bannedTopics pascalCase should match uppercase ban", func(t *testing.T) { | 
|  | 1326 | +		opt := WithoutBannedTopics([]string{"Chat"}) | 
|  | 1327 | +		err := opt(&restful.Request{}, nil, claims) | 
|  | 1328 | +		assert.Error(t, err) | 
|  | 1329 | +		svcErr, ok := err.(restful.ServiceError) | 
|  | 1330 | +		assert.True(t, ok) | 
|  | 1331 | +		assert.Equal(t, http.StatusForbidden, svcErr.Code) | 
|  | 1332 | +	}) | 
|  | 1333 | +} | 
0 commit comments