This is an automated email from the ASF dual-hosted git repository. xxyu pushed a commit to branch kylin5 in repository https://gitbox.apache.org/repos/asf/kylin.git
commit be5e5fa209fb26f8f5747ca3469403a46c8c6a13 Author: Liang.Hua <[email protected]> AuthorDate: Mon Oct 31 15:50:44 2022 +0800 KYLIN-5356 Backend configuration of users supports the project administrator role --- .../rest/response/OpenAccessGroupResponse.java | 40 +++++++ .../rest/response/OpenAccessUserResponse.java | 40 +++++++ .../rest/controller/v2/NAccessControllerV2.java | 124 ++++++++++++++++++-- .../rest/controller/NAccessControllerV2Test.java | 125 ++++++++++++++++++++- 4 files changed, 316 insertions(+), 13 deletions(-) diff --git a/src/common-service/src/main/java/org/apache/kylin/rest/response/OpenAccessGroupResponse.java b/src/common-service/src/main/java/org/apache/kylin/rest/response/OpenAccessGroupResponse.java new file mode 100644 index 0000000000..96042b432f --- /dev/null +++ b/src/common-service/src/main/java/org/apache/kylin/rest/response/OpenAccessGroupResponse.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.kylin.rest.response; + +import java.util.List; + +import org.apache.kylin.common.util.Pair; +import org.codehaus.jackson.annotate.JsonProperty; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class OpenAccessGroupResponse { + + @JsonProperty("groups") + private List<Pair<String, Integer>> groups; + + @JsonProperty("size") + private int size; +} diff --git a/src/common-service/src/main/java/org/apache/kylin/rest/response/OpenAccessUserResponse.java b/src/common-service/src/main/java/org/apache/kylin/rest/response/OpenAccessUserResponse.java new file mode 100644 index 0000000000..c7f046bc57 --- /dev/null +++ b/src/common-service/src/main/java/org/apache/kylin/rest/response/OpenAccessUserResponse.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.kylin.rest.response; + +import java.util.List; + +import org.codehaus.jackson.annotate.JsonProperty; + +import org.apache.kylin.metadata.user.ManagedUser; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class OpenAccessUserResponse { + + @JsonProperty("users") + private List<ManagedUser> users; + + @JsonProperty("size") + private int size; +} diff --git a/src/metadata-server/src/main/java/org/apache/kylin/rest/controller/v2/NAccessControllerV2.java b/src/metadata-server/src/main/java/org/apache/kylin/rest/controller/v2/NAccessControllerV2.java index 2b9f9d2f4b..e4465dd69f 100644 --- a/src/metadata-server/src/main/java/org/apache/kylin/rest/controller/v2/NAccessControllerV2.java +++ b/src/metadata-server/src/main/java/org/apache/kylin/rest/controller/v2/NAccessControllerV2.java @@ -17,8 +17,8 @@ */ package org.apache.kylin.rest.controller.v2; -import static org.apache.kylin.common.exception.ServerErrorCode.USER_NOT_EXIST; import static org.apache.kylin.common.constant.HttpConstant.HTTP_VND_APACHE_KYLIN_V2_JSON; +import static org.apache.kylin.common.exception.ServerErrorCode.USER_NOT_EXIST; import java.io.IOException; import java.util.ArrayList; @@ -26,22 +26,35 @@ import java.util.HashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; +import org.apache.commons.lang.StringUtils; +import org.apache.kylin.common.KylinConfig; import org.apache.kylin.common.exception.KylinException; import org.apache.kylin.common.persistence.AclEntity; +import org.apache.kylin.common.util.Pair; import org.apache.kylin.metadata.model.TableDesc; +import org.apache.kylin.metadata.project.NProjectManager; +import org.apache.kylin.metadata.project.ProjectInstance; import org.apache.kylin.rest.constant.Constant; +import org.apache.kylin.rest.controller.NBasicController; import org.apache.kylin.rest.response.AccessEntryResponse; import org.apache.kylin.rest.response.EnvelopeResponse; +import org.apache.kylin.rest.response.OpenAccessGroupResponse; +import org.apache.kylin.rest.response.OpenAccessUserResponse; +import org.apache.kylin.rest.security.AclEntityType; import org.apache.kylin.rest.service.AccessService; +import org.apache.kylin.rest.service.AclTCRService; +import org.apache.kylin.rest.service.IUserGroupService; import org.apache.kylin.rest.service.UserService; +import org.apache.kylin.rest.util.AclEvaluate; import org.apache.kylin.rest.util.PagingUtil; -import org.apache.kylin.rest.controller.NBasicController; -import org.apache.kylin.rest.service.AclTCRService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.security.acls.domain.GrantedAuthoritySid; +import org.springframework.security.acls.domain.PrincipalSid; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; @@ -49,6 +62,10 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.ResponseBody; +import com.google.common.collect.Lists; +import com.google.common.collect.Sets; + +import org.apache.kylin.metadata.user.ManagedUser; import io.swagger.annotations.ApiOperation; @Controller @@ -63,18 +80,26 @@ public class NAccessControllerV2 extends NBasicController { @Qualifier("userService") protected UserService userService; + @Autowired + @Qualifier("userGroupService") + private IUserGroupService userGroupService; + @Autowired @Qualifier("aclTCRService") private AclTCRService aclTCRService; + @Autowired + private AclEvaluate aclEvaluate; + private static final String PROJECT_NAME = "project_name"; private static final String TABLE_NAME = "table_name"; - private void checkUserName(String userName) { + private ManagedUser checkAndGetUser(String userName) { if (!userService.userExists(userName)) { throw new KylinException(USER_NOT_EXIST, String.format(Locale.ROOT, "User '%s' does not exists.", userName)); } + return (ManagedUser) userService.loadUserByUsername(userName); } /** @@ -89,7 +114,7 @@ public class NAccessControllerV2 extends NBasicController { @ResponseBody @PreAuthorize(Constant.ACCESS_HAS_ROLE_ADMIN) public EnvelopeResponse getAllAccessEntitiesOfUser(@PathVariable("userName") String username) throws IOException { - checkUserName(username); + checkAndGetUser(username); List<Object> dataList = new ArrayList<>(); List<String> projectList = accessService.getGrantedProjectsOfUser(username); @@ -115,13 +140,92 @@ public class NAccessControllerV2 extends NBasicController { @RequestParam(value = "pageOffset", required = false, defaultValue = "0") Integer pageOffset, @RequestParam(value = "pageSize", required = false, defaultValue = "10") Integer pageSize) throws IOException { - AclEntity ae = accessService.getAclEntity(type, getProject(project).getUuid()); - List<AccessEntryResponse> resultsAfterFuzzyMatching = this.accessService.generateAceResponsesByFuzzMatching(ae, - nameSeg, isCaseSensitive); - List<AccessEntryResponse> sublist = PagingUtil.cutPage(resultsAfterFuzzyMatching, pageOffset, pageSize); + List<AccessEntryResponse> accessList = getAccessList(type, project, nameSeg, isCaseSensitive); + List<AccessEntryResponse> sublist = PagingUtil.cutPage(accessList, pageOffset, pageSize); HashMap<String, Object> data = new HashMap<>(); data.put("sids", sublist); - data.put("size", resultsAfterFuzzyMatching.size()); + data.put("size", accessList.size()); return new EnvelopeResponse<>(KylinException.CODE_SUCCESS, data, ""); } + + @ApiOperation(value = "getAllAccessUsers", tags = { "MID" }) + @GetMapping(value = "/all/users", produces = { HTTP_VND_APACHE_KYLIN_V2_JSON }) + @ResponseBody + public EnvelopeResponse<OpenAccessUserResponse> getAllAccessUsers( + @RequestParam(value = "project", required = false) String project, + @RequestParam(value = "userName", required = false) String userName, + @RequestParam(value = "pageOffset", required = false, defaultValue = "0") Integer pageOffset, + @RequestParam(value = "pageSize", required = false, defaultValue = "10") Integer pageSize) + throws IOException { + Set<ManagedUser> users = StringUtils.isNotEmpty(userName) ? Sets.newHashSet(checkAndGetUser(userName)) + : getUsersOfProjects(getGrantedProjects(project)); + return new EnvelopeResponse<>(KylinException.CODE_SUCCESS, new OpenAccessUserResponse( + PagingUtil.cutPage(Lists.newArrayList(users), pageOffset, pageSize), users.size()), ""); + } + + @ApiOperation(value = "getAllAccessGroups", tags = { "MID" }) + @GetMapping(value = "/all/groups", produces = { HTTP_VND_APACHE_KYLIN_V2_JSON }) + @ResponseBody + public EnvelopeResponse<OpenAccessGroupResponse> getAllAccessGroups( + @RequestParam(value = "project", required = false) String project, + @RequestParam(value = "groupName", required = false) String groupName, + @RequestParam(value = "pageOffset", required = false, defaultValue = "0") Integer pageOffset, + @RequestParam(value = "pageSize", required = false, defaultValue = "10") Integer pageSize) + throws IOException { + List<Pair<String, Integer>> result = StringUtils.isNotEmpty(groupName) + ? Lists.newArrayList(Pair.newPair(groupName, userGroupService.getGroupMembersByName(groupName).size())) + : getUserGroupsOfProjects(getGrantedProjects(project)); + return new EnvelopeResponse<>(KylinException.CODE_SUCCESS, new OpenAccessGroupResponse( + PagingUtil.cutPage(Lists.newArrayList(result), pageOffset, pageSize), result.size()), ""); + } + + private List<AccessEntryResponse> getAccessList(String type, String projectName, String nameSeg, + boolean isCaseSensitive) throws IOException { + AclEntity aclEntity = accessService.getAclEntity(type, getProject(projectName).getUuid()); + return this.accessService.generateAceResponsesByFuzzMatching(aclEntity, nameSeg, isCaseSensitive); + } + + private List<String> getGrantedProjects(String projectName) { + NProjectManager projectManager = NProjectManager.getInstance(KylinConfig.getInstanceFromEnv()); + if (StringUtils.isBlank(projectName)) { + return projectManager.listAllProjects().stream().map(ProjectInstance::getName) + .filter(name -> aclEvaluate.hasProjectAdminPermission(name)).collect(Collectors.toList()); + } else if (aclEvaluate.hasProjectReadPermission(projectManager.getProject(projectName))) { + return Lists.newArrayList(projectName); + } + return Lists.newArrayList(); + } + + private Set<ManagedUser> getUsersOfProjects(List<String> projects) throws IOException { + Set<ManagedUser> allUsers = Sets.newHashSet(); + for (String projectName : projects) { + List<AccessEntryResponse> responses = getAccessList(AclEntityType.PROJECT_INSTANCE, projectName, null, + false); + allUsers.addAll(responses.stream().filter(response -> response.getSid() instanceof PrincipalSid) + .map(response -> (ManagedUser) userService + .loadUserByUsername(((PrincipalSid) response.getSid()).getPrincipal())) + .collect(Collectors.toSet())); + } + return allUsers; + } + + private List<Pair<String, Integer>> getUserGroupsOfProjects(List<String> projects) throws IOException { + List<Pair<String, Integer>> allUserGroups = Lists.newArrayList(); + List<String> grantedGroups = Lists.newArrayList(); + for (String projectName : projects) { + List<AccessEntryResponse> responses = getAccessList(AclEntityType.PROJECT_INSTANCE, projectName, null, + false); + for (AccessEntryResponse response : responses) { + if (response.getSid() instanceof GrantedAuthoritySid) { + String grantedAuthority = ((GrantedAuthoritySid) response.getSid()).getGrantedAuthority(); + if (!grantedGroups.contains(grantedAuthority)) { + grantedGroups.add(grantedAuthority); + allUserGroups.add(Pair.newPair(grantedAuthority, + userGroupService.getGroupMembersByName(grantedAuthority).size())); + } + } + } + } + return allUserGroups; + } } diff --git a/src/metadata-server/src/test/java/org/apache/kylin/rest/controller/NAccessControllerV2Test.java b/src/metadata-server/src/test/java/org/apache/kylin/rest/controller/NAccessControllerV2Test.java index 6aae71d82b..526ab7251d 100644 --- a/src/metadata-server/src/test/java/org/apache/kylin/rest/controller/NAccessControllerV2Test.java +++ b/src/metadata-server/src/test/java/org/apache/kylin/rest/controller/NAccessControllerV2Test.java @@ -19,12 +19,30 @@ package org.apache.kylin.rest.controller; import static org.apache.kylin.common.constant.HttpConstant.HTTP_VND_APACHE_KYLIN_V2_JSON; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import org.apache.kylin.common.persistence.AclEntity; +import org.apache.kylin.common.persistence.RootPersistentEntity; +import org.apache.kylin.common.util.NLocalFileMetadataTestCase; +import org.apache.kylin.metadata.project.NProjectManager; +import org.apache.kylin.metadata.project.ProjectInstance; import org.apache.kylin.rest.constant.Constant; -import org.apache.kylin.rest.service.AccessService; -import org.apache.kylin.rest.service.UserService; import org.apache.kylin.rest.controller.v2.NAccessControllerV2; +import org.apache.kylin.rest.response.AccessEntryResponse; +import org.apache.kylin.rest.response.EnvelopeResponse; +import org.apache.kylin.rest.response.OpenAccessGroupResponse; +import org.apache.kylin.rest.response.OpenAccessUserResponse; +import org.apache.kylin.rest.security.AclEntityType; +import org.apache.kylin.rest.service.AccessService; import org.apache.kylin.rest.service.AclTCRService; +import org.apache.kylin.rest.service.IUserGroupService; +import org.apache.kylin.rest.service.ProjectService; +import org.apache.kylin.rest.service.UserService; +import org.apache.kylin.rest.util.AclEvaluate; import org.junit.After; +import org.junit.Assert; import org.junit.Before; import org.junit.Test; import org.mockito.InjectMocks; @@ -32,8 +50,11 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.springframework.http.MediaType; +import org.springframework.security.acls.domain.GrantedAuthoritySid; +import org.springframework.security.acls.domain.PrincipalSid; import org.springframework.security.authentication.TestingAuthenticationToken; import org.springframework.security.core.Authentication; +import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.request.MockMvcRequestBuilders; @@ -42,7 +63,9 @@ import org.springframework.test.web.servlet.setup.MockMvcBuilders; import com.google.common.collect.Lists; -public class NAccessControllerV2Test { +import org.apache.kylin.metadata.user.ManagedUser; + +public class NAccessControllerV2Test extends NLocalFileMetadataTestCase { private MockMvc mockMvc; @@ -55,6 +78,15 @@ public class NAccessControllerV2Test { @Mock private AclTCRService aclTCRService; + @Mock + private AclEvaluate aclEvaluate; + + @Mock + private ProjectService projectService; + + @Mock + private IUserGroupService userGroupService; + @InjectMocks private NAccessControllerV2 nAccessControllerV2 = Mockito.spy(new NAccessControllerV2()); @@ -67,10 +99,12 @@ public class NAccessControllerV2Test { .defaultRequest(MockMvcRequestBuilders.get("/")).build(); SecurityContextHolder.getContext().setAuthentication(authentication); + createTestMetadata(); } @After public void tearDown() { + cleanupTestMetadata(); } @Test @@ -88,4 +122,89 @@ public class NAccessControllerV2Test { Mockito.verify(nAccessControllerV2).getAllAccessEntitiesOfUser(userName); } + @Test + public void testGetAllAccessUsers() throws Exception { + String project = "default"; + String userName = "user01"; + mockMvc.perform(MockMvcRequestBuilders.get("/api/access/all/users").contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.parseMediaType(HTTP_VND_APACHE_KYLIN_V2_JSON))) + .andExpect(MockMvcResultMatchers.status().isOk()).andReturn(); + + Mockito.verify(nAccessControllerV2).getAllAccessUsers(null, null, 0, 10); + + Mockito.doNothing().when(aclEvaluate).checkProjectAdminPermission(project); + mockMvc.perform(MockMvcRequestBuilders.get("/api/access/all/users").param("project", project) + .param("userName", userName).contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.parseMediaType(HTTP_VND_APACHE_KYLIN_V2_JSON))) + .andExpect(MockMvcResultMatchers.status().is5xxServerError()).andReturn(); + + Mockito.verify(nAccessControllerV2).getAllAccessUsers(project, userName, 0, 10); + + List<GrantedAuthority> authorities = new ArrayList<>(); + ManagedUser user = new ManagedUser(userName, "123", false, authorities); + Authentication authentication = new TestingAuthenticationToken(user, userName, Constant.ROLE_ADMIN); + SecurityContextHolder.getContext().setAuthentication(authentication); + Mockito.doReturn(user).when(userService).loadUserByUsername(userName); + try { + nAccessControllerV2.getAllAccessUsers(project, userName, 0, 10); + } catch (Exception e) { + Assert.assertEquals(String.format(Locale.ROOT, "User '%s' does not exists.", userName), e.getMessage()); + } + + ProjectInstance projectInstance = NProjectManager.getInstance(getTestConfig()).getProject(project); + Mockito.doReturn(Lists.newArrayList(projectInstance)).when(projectService).getReadableProjects(project, true); + AccessEntryResponse accessEntryResponse = Mockito.mock(AccessEntryResponse.class); + AclEntity aclEntity = Mockito.mock(RootPersistentEntity.class); + PrincipalSid principalSid = Mockito.mock(PrincipalSid.class); + Mockito.doReturn(principalSid).when(accessEntryResponse).getSid(); + Mockito.doReturn(userName).when(principalSid).getPrincipal(); + Mockito.doReturn(aclEntity).when(accessService).getAclEntity(AclEntityType.PROJECT_INSTANCE, + projectInstance.getUuid()); + Mockito.doReturn(Lists.newArrayList(accessEntryResponse)).when(accessService) + .generateAceResponsesByFuzzMatching(aclEntity, null, false); + EnvelopeResponse<OpenAccessUserResponse> envelopeResponse1 = nAccessControllerV2.getAllAccessUsers(project, + null, 0, 10); + Assert.assertNotNull(envelopeResponse1.getData()); + } + + @Test + public void testGetAllAccessGroups() throws Exception { + String project = "default"; + String groupName = "group01"; + mockMvc.perform(MockMvcRequestBuilders.get("/api/access/all/groups").contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.parseMediaType(HTTP_VND_APACHE_KYLIN_V2_JSON))) + .andExpect(MockMvcResultMatchers.status().isOk()).andReturn(); + Mockito.verify(nAccessControllerV2).getAllAccessGroups(null, null, 0, 10); + + mockMvc.perform(MockMvcRequestBuilders.get("/api/access/all/groups").param("project", project) + .param("groupName", groupName).contentType(MediaType.APPLICATION_JSON) + .accept(MediaType.parseMediaType(HTTP_VND_APACHE_KYLIN_V2_JSON))) + .andExpect(MockMvcResultMatchers.status().isOk()).andReturn(); + + Mockito.verify(nAccessControllerV2).getAllAccessGroups(project, groupName, 0, 10); + + List<GrantedAuthority> authorities = new ArrayList<>(); + ManagedUser user = new ManagedUser("user", "123", false, authorities); + Authentication authentication = new TestingAuthenticationToken(user, "user", Constant.ROLE_ADMIN); + Mockito.doReturn(Lists.newArrayList(user)).when(userGroupService).getGroupMembersByName(groupName); + EnvelopeResponse<OpenAccessGroupResponse> envelopeResponse = nAccessControllerV2.getAllAccessGroups(project, + groupName, 0, 10); + Assert.assertNotNull(envelopeResponse.getData()); + + ProjectInstance projectInstance = NProjectManager.getInstance(getTestConfig()).getProject(project); + Mockito.doReturn(Lists.newArrayList(projectInstance)).when(projectService).getReadableProjects(project, true); + AccessEntryResponse accessEntryResponse = Mockito.mock(AccessEntryResponse.class); + AclEntity aclEntity = Mockito.mock(RootPersistentEntity.class); + GrantedAuthoritySid grantedAuthoritySid = Mockito.mock(GrantedAuthoritySid.class); + Mockito.doReturn(grantedAuthoritySid).when(accessEntryResponse).getSid(); + Mockito.doReturn(groupName).when(grantedAuthoritySid).getGrantedAuthority(); + Mockito.doReturn(aclEntity).when(accessService).getAclEntity(AclEntityType.PROJECT_INSTANCE, + projectInstance.getUuid()); + Mockito.doReturn(Lists.newArrayList(accessEntryResponse)).when(accessService) + .generateAceResponsesByFuzzMatching(aclEntity, null, false); + EnvelopeResponse<OpenAccessGroupResponse> envelopeResponse1 = nAccessControllerV2.getAllAccessGroups(project, + null, 0, 10); + Assert.assertNotNull(envelopeResponse1.getData()); + } + }
