Skip to content

Commit 656fcdc

Browse files
authored
Merge pull request #298 from kbss-cvut/impersonate-role
Impersonate role
2 parents c6f8020 + 529daba commit 656fcdc

File tree

6 files changed

+83
-10
lines changed

6 files changed

+83
-10
lines changed

deploy/internal-auth/db-server/import/record-manager-app/role-groups.trig

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
@prefix ufo: <http://onto.fel.cvut.cz/ontologies/ufo/> .
1111

1212
<http://onto.fel.cvut.cz/ontologies/record-manager/role-groups> {
13+
1314
rm:admin-role-group rdf:type owl:NamedIndividual, rm:role-group;
1415
rm:has-role rm:read-all-records-role,
1516
rm:write-all-records-role,
@@ -29,8 +30,25 @@
2930
rm:read-all-organizations-role,
3031
rm:write-all-organizations-role,
3132
rm:read-action-history-role,
32-
rm:read-statistics-role;
33+
rm:read-statistics-role,
34+
rm:impersonate-role;
3335
rdfs:label "admin-role-group"@en .
36+
37+
rm:data-collection-coordinator-impersonate-role-group rdf:type owl:NamedIndividual, rm:role-group;
38+
rm:has-role rm:read-all-users-role,
39+
rm:write-all-users-role,
40+
rm:read-organization-users-role,
41+
rm:write-organization-users-role,
42+
rm:read-organization-role,
43+
rm:write-organization-role,
44+
rm:read-all-organizations-role,
45+
rm:write-all-organizations-role,
46+
rm:read-organization-records-role,
47+
rm:write-organization-records-role,
48+
rm:comment-record-questions-role,
49+
rm:complete-records-role,
50+
rm:impersonate-role;
51+
rdfs:label "data-collection-coordinator-impersonate-role-group"@en .
3452

3553
rm:data-collection-coordinator-role-group rdf:type owl:NamedIndividual, rm:role-group;
3654
rm:has-role rm:read-all-users-role,
@@ -45,9 +63,19 @@
4563
rm:write-organization-records-role,
4664
rm:comment-record-questions-role,
4765
rm:complete-records-role;
48-
4966
rdfs:label "data-collection-coordinator-role-group"@en .
5067

68+
rm:organization-manager-impersonate-role-group rdf:type owl:NamedIndividual, rm:role-group;
69+
rm:has-role rm:read-organization-role,
70+
rm:write-organization-role,
71+
rm:read-organization-users-role,
72+
rm:write-organization-users-role,
73+
rm:read-organization-records-role,
74+
rm:write-organization-records-role,
75+
rm:comment-record-questions-role,
76+
rm:impersonate-role;
77+
rdfs:label "organization-manager-impersonate-role-group"@en .
78+
5179
rm:organization-manager-role-group rdf:type owl:NamedIndividual, rm:role-group;
5280
rm:has-role rm:read-organization-role,
5381
rm:write-organization-role,
@@ -58,12 +86,25 @@
5886
rm:comment-record-questions-role;
5987
rdfs:label "organization-manager-role-group"@en .
6088

89+
rm:entry-clerk-impersonate-role-group rdf:type owl:NamedIndividual, rm:role-group;
90+
rm:has-role rm:read-organization-role,
91+
rm:read-organization-records-role,
92+
rm:comment-record-questions-role,
93+
rm:impersonate-role;
94+
rdfs:label "entry-clerk-impersonate-role-group"@en .
95+
6196
rm:entry-clerk-role-group rdf:type owl:NamedIndividual, rm:role-group;
6297
rm:has-role rm:read-organization-role,
6398
rm:read-organization-records-role,
6499
rm:comment-record-questions-role;
65100
rdfs:label "entry-clerk-role-group"@en .
66101

102+
rm:reviewer-impersonate-role-group rdf:type owl:NamedIndividual, rm:role-group;
103+
rm:has-role rm:complete-records-role,
104+
rm:comment-record-questions-role,
105+
rm:impersonate-role;
106+
rdfs:label "reviewer-role-group"@en .
107+
67108
rm:reviewer-role-group rdf:type owl:NamedIndividual, rm:role-group;
68109
rm:has-role rm:complete-records-role,
69110
rm:comment-record-questions-role;

deploy/keycloak-auth/keycloak-config/roles.tf

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,11 +61,30 @@ EOT
6161
}
6262
}
6363

64-
6564
resource "keycloak_role" "realm_roles" {
6665
for_each = var.roles
6766

6867
realm_id = var.kc_realm
6968
name = each.key
7069
description = length(each.value) > 0 ? each.value : null
71-
}
70+
}
71+
72+
# --- Impersonation role composite ---
73+
data "keycloak_openid_client" "realm_management" {
74+
realm_id = var.kc_realm
75+
client_id = "realm-management"
76+
}
77+
78+
data "keycloak_role" "realm_management_impersonation" {
79+
realm_id = var.kc_realm
80+
client_id = data.keycloak_openid_client.realm_management.id
81+
name = "impersonation"
82+
}
83+
84+
resource "keycloak_role" "impersonate_role_composite" {
85+
realm_id = var.kc_realm
86+
name = "impersonate-role"
87+
composite_roles = [
88+
data.keycloak_role.realm_management_impersonation.id
89+
]
90+
}

src/components/user/User.jsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { FaRandom } from "react-icons/fa";
1313
import { isUsingOidcAuth } from "../../utils/OidcUtils";
1414
import IfInternalAuth from "../misc/oidc/IfInternalAuth.jsx";
1515
import RoleBadges from "../RoleBadges.jsx";
16-
import { canWriteUserInfo, getRoles, hasSupersetOfPrivileges, hasRole } from "../../utils/RoleUtils.js";
16+
import { canWriteUserInfo, getRoles, hasSupersetOfPrivileges, hasRole, canImpersonate } from "../../utils/RoleUtils.js";
1717
import RoleGroupsSelector from "../RoleGroupsSelector.jsx";
1818
import InstitutionSelector from "../institution/InstitutionSelector.jsx";
1919

@@ -160,7 +160,7 @@ class User extends React.Component {
160160
_impersonateButton() {
161161
const { user, currentUser, handlers, impersonation } = this.props;
162162

163-
if (!user.isNew && hasSupersetOfPrivileges(currentUser, user) && currentUser.username !== user.username) {
163+
if (!user.isNew && canImpersonate(currentUser, user)) {
164164
return (
165165
<Button
166166
style={{ margin: "0 0.3em 0 0" }}

src/constants/DefaultConstants.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,9 @@ export const ROLE = {
113113
COMMENT_RECORD_QUESTIONS: "commentRecordQuestions",
114114
READ_ACTION_HISTORY: "readActionHistory",
115115
READ_STATISTICS: "readStatistics",
116+
117+
// Impersonate
118+
IMPERSONATE: "impersonate",
116119
};
117120

118121
// Default number of table elements per page.

src/utils/RoleUtils.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,14 @@ export function canSelectInstitution(currentUser, user) {
102102
);
103103
}
104104

105+
export function canImpersonate(currentUser, user) {
106+
return (
107+
hasRole(currentUser, ROLE.IMPERSONATE) &&
108+
hasSupersetOfPrivileges(currentUser, user) &&
109+
currentUser.username !== user.username
110+
);
111+
}
112+
105113
export function canCreateInstitution(currentUser) {
106114
return hasRole(currentUser, ROLE.WRITE_ALL_ORGANIZATIONS);
107115
}

tests/__tests__/components/User.spec.jsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import User from "../../../src/components/user/User";
44
import { describe, expect, it, vi, beforeEach } from "vitest";
55
import { getMessageByKey, renderWithIntl } from "../../utils/utils.jsx";
66
import { isUsingOidcAuth } from "../../../src/utils/OidcUtils.js";
7-
import { canWriteUserInfo, hasSupersetOfPrivileges } from "../../../src/utils/RoleUtils.js";
7+
import { canImpersonate, canWriteUserInfo, hasSupersetOfPrivileges } from "../../../src/utils/RoleUtils.js";
88
import UserValidator from "../../../src/validation/UserValidator.jsx";
99
import { ACTION_STATUS } from "../../../src/constants/DefaultConstants.js";
1010

@@ -43,6 +43,7 @@ vi.mock("../../../src/utils/RoleUtils", () => ({
4343
getRoles: vi.fn(),
4444
hasRole: vi.fn(),
4545
hasSupersetOfPrivileges: vi.fn(),
46+
canImpersonate: vi.fn(),
4647
}));
4748

4849
vi.mock("../../../src/components/Loader", () => ({
@@ -76,6 +77,7 @@ describe("User", function () {
7677
canWriteUserInfo.mockReturnValue(true);
7778
hasSupersetOfPrivileges.mockReturnValue(true);
7879
UserValidator.isValid.mockReturnValue(true);
80+
canImpersonate.mockReturnValue(true);
7981
});
8082

8183
it("should render loading state when user is not provided", () => {
@@ -149,13 +151,13 @@ describe("User", function () {
149151
expect(screen.getByText(getMessageByKey("save-and-send-email"))).toBeDisabled();
150152
});
151153

152-
it("renders impersonate button when currentUser has superset of roles of another user", () => {
154+
it("renders impersonate button when currentUser has canImpersonate permission", () => {
153155
renderComponent();
154156
expect(screen.getByText(getMessageByKey("user.impersonate"))).toBeInTheDocument();
155157
});
156158

157-
it("does not render impersonate button when currentUser lacks superset of roles of another user", () => {
158-
hasSupersetOfPrivileges.mockReturnValue(false);
159+
it("does not render impersonate button when currentUser lacks canImpersonate permission", () => {
160+
canImpersonate.mockReturnValue(false);
159161
renderComponent();
160162
expect(screen.queryByText(getMessageByKey("user.impersonate"))).not.toBeInTheDocument();
161163
});

0 commit comments

Comments
 (0)