Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,16 @@
import com.microsoft.hydralab.center.service.SecurityUserService;
import com.microsoft.hydralab.center.service.SysTeamService;
import com.microsoft.hydralab.center.service.SysUserService;
import com.microsoft.hydralab.center.service.TeamAppManagementService;
import com.microsoft.hydralab.center.service.UserTeamManagementService;
import com.microsoft.hydralab.common.entity.agent.Result;
import com.microsoft.hydralab.common.entity.center.SysTeam;
import com.microsoft.hydralab.common.entity.center.SysUser;
import com.microsoft.hydralab.common.entity.center.TeamAppRelation;
import com.microsoft.hydralab.common.entity.center.UserTeamRelation;
import com.microsoft.hydralab.common.util.Const;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.security.access.prepost.PreAuthorize;
Expand All @@ -29,7 +32,7 @@

@RestController
@RequestMapping
public class UserTeamController {
public class TeamController {
@Resource
SysTeamService sysTeamService;
@Resource
Expand All @@ -38,6 +41,8 @@ public class UserTeamController {
UserTeamManagementService userTeamManagementService;
@Resource
SecurityUserService securityUserService;
@Autowired
private TeamAppManagementService teamAppManagementService;

@PreAuthorize("hasAnyAuthority('SUPER_ADMIN','ADMIN')")
@PostMapping(value = {"/api/team/create"}, produces = MediaType.APPLICATION_JSON_VALUE)
Expand Down Expand Up @@ -311,4 +316,70 @@ public Result<SysTeam> queryDefaultTeam(@CurrentSecurityContext SysUser requesto
return Result.ok(sysTeamService.queryTeamById(user.getDefaultTeamId()));
}

/**
* Authenticated USER: all
*/
@PostMapping(value = {"/api/teamApp/addRelation"}, produces = MediaType.APPLICATION_JSON_VALUE)
public Result<TeamAppRelation> addTeamAppRelation(@CurrentSecurityContext SysUser requestor,
@RequestParam("appClientId") String appClientId,
@RequestParam("teamId") String teamId) {
if (requestor == null) {
return Result.error(HttpStatus.UNAUTHORIZED.value(), "Unauthorized");
}
/// todo: app client ID format check
if (!sysUserService.checkUserAdmin(requestor) && !userTeamManagementService.checkRequestorTeamRelation(requestor, teamId)) {
return Result.error(HttpStatus.UNAUTHORIZED.value(), "Unauthorized, user doesn't belong to this Team");
}
String existingTeamId = teamAppManagementService.queryTeamIdByClientId(appClientId);
if (existingTeamId != null) {
if (existingTeamId.equals(teamId)) {
return Result.error(HttpStatus.FORBIDDEN.value(), "Client ID already linked in current team.");
} else {
return Result.error(HttpStatus.FORBIDDEN.value(), "Client ID already linked in another team.");
}
}

return Result.ok(teamAppManagementService.addTeamAppRelation(teamId, appClientId));
}

/**
* Authenticated USER: all
*/
@PostMapping(value = {"/api/teamApp/deleteRelation"}, produces = MediaType.APPLICATION_JSON_VALUE)
public Result deleteTeamAppRelation(@CurrentSecurityContext SysUser requestor,
@RequestParam("appClientId") String appClientId,
@RequestParam("teamId") String teamId) {
if (requestor == null) {
return Result.error(HttpStatus.UNAUTHORIZED.value(), "Unauthorized");
}
/// todo: app client ID format check
if (!sysUserService.checkUserAdmin(requestor) && !userTeamManagementService.checkRequestorTeamRelation(requestor, teamId)) {
return Result.error(HttpStatus.UNAUTHORIZED.value(), "Unauthorized, user doesn't belong to this Team");
}
TeamAppRelation relation = teamAppManagementService.queryRelation(appClientId, teamId);
if (relation == null) {
return Result.error(HttpStatus.BAD_REQUEST.value(), "Relation doesn't exist.");
}

teamAppManagementService.deleteTeamAppRelation(relation);
return Result.ok("delete team-app relation successfully!");
}

/**
* Authenticated USER: all
*/
@PostMapping(value = {"/api/team/clientIds"}, produces = MediaType.APPLICATION_JSON_VALUE)
public Result<List<String>> queryTeamClientIds(@CurrentSecurityContext SysUser requestor,
@RequestParam("teamId") String teamId) {
if (requestor == null) {
return Result.error(HttpStatus.UNAUTHORIZED.value(), "Unauthorized");
}
/// todo: app client ID format check
if (!sysUserService.checkUserAdmin(requestor) && !userTeamManagementService.checkRequestorTeamRelation(requestor, teamId)) {
return Result.error(HttpStatus.UNAUTHORIZED.value(), "Unauthorized, user doesn't belong to this Team");
}

List<String> clientIds = teamAppManagementService.queryClientIdsByTeam(teamId);
return Result.ok(clientIds);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,16 @@ public class BaseInterceptor extends HandlerInterceptorAdapter {
AuthTokenService authTokenService;
@Value("${app.storage.type}")
private String storageType;
@Value("${app.api-auth-mode}")
private String apiAuthMode;

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
String remoteUser = request.getRemoteUser();
String requestURI = request.getRequestURI();
String oauthToken = null;
String sessionAuthToken = null;
String authorizationToken;
String aadIdToken;
if (LogUtils.isLegalStr(requestURI, Const.RegexString.URL, true) && LogUtils.isLegalStr(remoteUser, Const.RegexString.MAIL_ADDRESS, true)) {
LOGGER.info("New access from IP {}, host {}, user {}, for path {}", request.getRemoteAddr(), request.getRemoteHost(), remoteUser,
requestURI);// CodeQL [java/log-injection] False Positive: Has verified the string by regular expression
Expand All @@ -64,41 +68,43 @@ public boolean preHandle(HttpServletRequest request, HttpServletResponse respons
SecurityContext securityContext = (SecurityContext) request.getSession().getAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY);
if (securityContext != null) {
SysUser userAuthentication = (SysUser) securityContext.getAuthentication();
oauthToken = userAuthentication.getAccessToken();
sessionAuthToken = userAuthentication.getAccessToken();
}

String authToken = request.getHeader("Authorization");
if (authToken != null) {
authToken = authToken.replaceAll("Bearer ", "");
authorizationToken = request.getHeader("Authorization");
if (authorizationToken != null) {
authorizationToken = authorizationToken.replaceAll("Bearer ", "");
}

// For Azure AD authentication
String accessToken = request.getHeader("X-MS-TOKEN-AAD-ID-TOKEN");
aadIdToken = request.getHeader("X-MS-TOKEN-AAD-ID-TOKEN");
LOGGER.info("UserId: " + request.getHeader("X-MS-CLIENT-PRINCIPAL-ID"));
LOGGER.info("UserName: " + request.getHeader("X-MS-CLIENT-PRINCIPAL-NAME"));

//check is ignore
// check is ignore
if (!authUtil.isIgnore(requestURI)) {
//invoked by API client
if (!StringUtils.isEmpty(accessToken)) {
if (authTokenService.checkAADToken(accessToken)) {
// invoked by API client
if (!StringUtils.isEmpty(aadIdToken)) {
if (authTokenService.checkAADToken(aadIdToken)) {
return true;
} else {
response.sendError(HttpStatus.UNAUTHORIZED.value(), "unauthorized, error authorization code");
}
}

//invoke by client
if (!StringUtils.isEmpty(authToken)) {
if (authTokenService.checkAuthToken(authToken)) {
// invoked by agent client
if (!StringUtils.isEmpty(authorizationToken)) {
if ("SECRET".equals(apiAuthMode) && authTokenService.checkAuthToken(authorizationToken)) {
return true;
} else if ("TOKEN".equals(apiAuthMode) && authTokenService.setUserAuthByAppClientToken(authorizationToken)) {
return true;
} else {
response.sendError(HttpStatus.UNAUTHORIZED.value(), "unauthorized, error authorization code");
}

}
//invoke by browser
if (StringUtils.isEmpty(oauthToken) || !authUtil.verifyToken(oauthToken)) {

// invoke by browser
if (StringUtils.isEmpty(sessionAuthToken) || !authUtil.verifyToken(sessionAuthToken)) {
if (requestURI.contains(Const.FrontEndPath.PREFIX_PATH)) {
String queryString = request.getQueryString();
if (StringUtils.isNotEmpty(queryString)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

package com.microsoft.hydralab.center.repository;

import com.microsoft.hydralab.common.entity.center.TeamAppRelation;
import com.microsoft.hydralab.common.entity.center.TeamAppRelationId;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface TeamAppRelationRepository extends JpaRepository<TeamAppRelation, TeamAppRelationId> {
Optional<TeamAppRelation> findByAppClientIdAndTeamId(String appClientId, String teamId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.microsoft.hydralab.center.repository.AuthTokenRepository;
import com.microsoft.hydralab.center.util.AuthUtil;
import com.microsoft.hydralab.common.entity.center.AuthToken;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
Expand All @@ -24,6 +25,10 @@ public class AuthTokenService {
AuthTokenRepository authTokenRepository;
@Resource
SecurityUserService securityUserService;
@Autowired
private TeamAppManagementService teamAppManagementService;
@Autowired
private UserTeamManagementService userTeamManagementService;

public AuthToken saveAuthToken(AuthToken authToken) {
return authTokenRepository.save(authToken);
Expand Down Expand Up @@ -76,6 +81,23 @@ public boolean checkAADToken(String aadToken) {
return true;
}

public boolean setUserAuthByAppClientToken(String clientAadToken) {
if (!authUtil.isValidToken(clientAadToken)) {
return false;
}
String appClientId = authUtil.getAppClientId(clientAadToken);
String teamId = teamAppManagementService.queryTeamIdByClientId(appClientId);
Authentication authObj = userTeamManagementService.queryUsersByTeam(teamId).stream()
.findFirst()
.orElse(null);
if (authObj == null) {
return false;
}

SecurityContextHolder.getContext().setAuthentication(authObj);
return true;
}

public void loadDefaultUser(HttpSession session) {
securityUserService.addDefaultUserSession(session);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

package com.microsoft.hydralab.center.service;

import com.microsoft.hydralab.center.repository.TeamAppRelationRepository;
import com.microsoft.hydralab.common.entity.center.TeamAppRelation;
import org.springframework.stereotype.Service;

import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;

@Service
public class TeamAppManagementService {
// cache teamId -> App Client ID mapping <String, String>
private final Map<String, Set<String>> teamAppListMap = new ConcurrentHashMap<>();
// cache App Client ID -> teamId mapping <String, String>
private final Map<String, String> appTeamListMap = new ConcurrentHashMap<>();
@Resource
private TeamAppRelationRepository teamAppRelationRepository;

@PostConstruct
public void initList() {
List<TeamAppRelation> relationList = teamAppRelationRepository.findAll();
relationList.forEach(relation -> {
this.insertToCache(relation.getTeamId(), relation.getAppClientId());
});
}

private void insertToCache(String teamId, String appClientId) {
Set<String> clientIds = teamAppListMap.computeIfAbsent(teamId, k -> new HashSet<>());
clientIds.add(appClientId);

appTeamListMap.put(appClientId, teamId);
}

private void removeFromCache(TeamAppRelation relation) {
Set<String> clientIdList = teamAppListMap.get(relation.getTeamId());
if (clientIdList != null) {
clientIdList.removeIf(clientId -> clientId.equals(relation.getAppClientId()));
}
appTeamListMap.remove(relation.getAppClientId());
}

public TeamAppRelation addTeamAppRelation(String teamId, String appClientId) {
this.insertToCache(teamId, appClientId);

TeamAppRelation teamAppRelation = new TeamAppRelation(teamId, appClientId);
return teamAppRelationRepository.save(teamAppRelation);
}

public void deleteTeamAppRelation(TeamAppRelation relation) {
removeFromCache(relation);
teamAppRelationRepository.delete(relation);
}

public TeamAppRelation queryRelation(String appClientId, String teamId) {
return teamAppRelationRepository.findByAppClientIdAndTeamId(appClientId, teamId).orElse(null);
}

public List<String> queryClientIdsByTeam(String teamId) {
Set<String> clientIds = teamAppListMap.get(teamId);
if (clientIds == null) {
return null;
}

return new ArrayList<>(clientIds);
}

public String queryTeamIdByClientId(String appClientId) {
return appTeamListMap.get(appClientId);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,15 @@ private PublicKey getPublicKey(JWSObject jwsObject, JWKSet jwkSet) throws JOSEEx
return publicKey;
}

public String getAppClientId(String accessToken) {
String clientId = "";
JSONObject clientInfo = decodeAccessToken(accessToken);
if (clientInfo != null) {
clientId = clientInfo.getString("appid");
}
return clientId;
}

/**
* check the uri is need verify auth
*
Expand Down
1 change: 1 addition & 0 deletions center/src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ app:
deployment: ${OPENAI_DEPLOYMENT:}
endpoint: ${OPENAI_ENDPOINT:}
agent-auth-mode: ${AGENT_AUTH_MODE:SECRET} # options: TOKEN, SECRET
api-auth-mode: ${API_AUTH_MODE:SECRET} # options: TOKEN, SECRET
management:
endpoints:
web:
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
package com.microsoft.hydralab.common.entity.center;

import lombok.Data;
import lombok.NoArgsConstructor;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.IdClass;
import javax.persistence.Table;
import java.io.Serializable;

@Data
@Entity
@NoArgsConstructor
@Table(name = "team_app_relation")
@IdClass(TeamAppRelationId.class)
public class TeamAppRelation implements Serializable {
private static final long serialVersionUID = 1L;

@Id
private String teamId;
@Id
@Column(unique = true)
private String appClientId;

public TeamAppRelation(String teamId, String appClientId){
this.teamId = teamId;
this.appClientId = appClientId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
package com.microsoft.hydralab.common.entity.center;

import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;


@Data
@NoArgsConstructor
public class TeamAppRelationId implements Serializable {
private String appClientId;
private String teamId;
}
Loading
Loading