1+ /*
2+ * Author: Cristian Gonzalez & Ignacio Roncero
3+ * Since: July 2025
4+ * Description: This Groovy script is designed to perform a comprehensive audit of a specific user's
5+ * **Role-Based Access Control (RBAC)** permissions within a CloudBees Core CI environment,
6+ * tracing those permissions from a specified Jenkins item up through all inherited contexts.
7+ *
8+ * It effectively answers the question: "What permissions does 'username' have on 'itemPath',
9+ * and where do those permissions come from?"
10+ *
11+ * Key Functions:
12+ * ---------------------------------------------------------------------------------------
13+ * 1. Role and Group Tracing:
14+ * - The script iterates from the target item (`itemPath`) up to the Jenkins root,
15+ * examining the RBAC Group Container at every level (folder, job, root).
16+ * 2. User-to-Group Resolution:
17+ * - It identifies all groups the specified `username` belongs to at the current context level.
18+ * 3. Ancestry Resolution:
19+ * - It traces and resolves the full **nested hierarchy** of groups to ensure all
20+ * inherited roles (from parent groups) are captured.
21+ * 4. Permission Collection:
22+ * - For every inherited role, it retrieves the associated **explicit permissions**
23+ * (e.g., Job/Read, View/Configure).
24+ * 5. Deduplication:
25+ * - It includes logic to prevent listing the same role's permissions multiple times
26+ * if it is inherited through multiple group paths or contexts.
27+ *
28+ * The final output provides a clear, categorized summary of every unique RBAC role applied to the
29+ * user, detailing the group they belong to, the full group path, and the context (Jenkins root or specific item)
30+ * where that role was assigned.
31+ */
32+
33+
34+ import jenkins.model.Jenkins
35+ import hudson.model.Item
36+ import nectar.plugins.rbac.strategy.RoleMatrixAuthorizationPlugin
37+ import nectar.plugins.rbac.groups.GroupContainerLocator
38+ import nectar.plugins.rbac.roles.Role
39+ import nectar.plugins.rbac.groups.Group
40+ import hudson.security.Permission
41+
42+ // Set your Jenkins username and item path
43+ def username = " username"
44+ def itemPath = " /"
45+
46+ def checkingItems (username , itemPath ){
47+ def item = Jenkins . instance. getItemByFullName(itemPath)
48+ if (item == null ) {
49+ println " ❌ Item not found: '${ itemPath} '"
50+ return
51+ }
52+
53+ def roleDetails = [] // list of [roleName, groupName, path, contextName, permissions]
54+ def roleNameDetails = []
55+ def seenRoleKeys = [] as Set // To avoid duplicates: roleName + context + groupName
56+
57+ def current = item
58+ while (current != null ) {
59+ def contextLabel = (current == Jenkins . instance) ? " <root>" : current. fullName
60+ def groupContainer = (current == Jenkins . instance)
61+ ? RoleMatrixAuthorizationPlugin . getInstance(). getRootProxyGroupContainer()
62+ : GroupContainerLocator . locate(current)
63+
64+ if (groupContainer == null ) {
65+ println " ⚠️ No group container found at '${ contextLabel} '"
66+ current = (current instanceof Item ) ? current. getParent() : null
67+ continue
68+ }
69+
70+ // Build group and parent mappings
71+ def groupMap = groupContainer. getGroups(). collectEntries { [(it. name): it] }
72+ def parentMap = [:]. withDefault { [] }
73+
74+ groupContainer. getGroups(). each { group ->
75+ group. getGroups(). each { nested ->
76+ parentMap[nested] << group. name
77+ }
78+ }
79+
80+ // Find user's direct groups
81+ def userGroups = groupContainer. getGroups(). findAll { g ->
82+ g. metaClass. respondsTo(g, " getUsers" ) && g. getUsers(). contains(username)
83+ }
84+
85+ // Resolve full nested paths
86+ def visitedPaths = [] as Set
87+ userGroups. each { group ->
88+ def start = group. name
89+ def stack = [[start]]
90+
91+ while (! stack. isEmpty()) {
92+ def path = stack. pop()
93+ def currentGroup = path[-1 ]
94+ visitedPaths << path
95+
96+ parentMap[currentGroup]. each { parent ->
97+ if (! path. contains(parent)) {
98+ stack. push(path + [parent])
99+ }
100+ }
101+ }
102+ }
103+
104+ // Collect roles and permissions
105+ visitedPaths. each { path ->
106+ def reversed = path. reverse()
107+ reversed. each { groupName ->
108+ def group = groupMap[groupName]
109+ if (group) {
110+ def roles = group. getAllRoles()
111+ roles. each { roleName ->
112+ def key = " ${ roleName} @${ contextLabel} @${ groupName} "
113+ if (seenRoleKeys. contains(key)) return
114+ seenRoleKeys << key
115+
116+ try {
117+ if (! roleNameDetails. contains(roleName)){
118+ def role = new Role (roleName)
119+ def perms = role. getPermissionProxies(). collect { " ${ it.group.title} / ${ it.name} " }
120+ roleDetails << [
121+ roleName : roleName,
122+ groupName : groupName,
123+ path : reversed. join(" → " ),
124+ context : contextLabel,
125+ permissions : perms
126+ ]
127+ roleNameDetails << roleName
128+ }
129+ } catch (Exception e) {
130+ println " ⚠️ Could not resolve role '${ roleName} ': ${ e.message} "
131+ }
132+ }
133+ }
134+ }
135+ }
136+
137+ // Move up
138+ current = (current instanceof Item ) ? current. getParent() : null
139+ if (current == Jenkins . instance) break
140+ }
141+
142+ // ✅ Final output
143+ if (roleDetails. isEmpty()) {
144+ println " ❌ No roles found for user '${ username} ' in '${ item.fullName} ' or inherited contexts."
145+ return
146+ }
147+
148+ println " \n RBAC Role & Permission Summary for '${ username} ' (including inherited scopes):\n "
149+
150+ roleDetails. each { entry ->
151+ println " 🔹 Role: ${ entry.roleName} "
152+ println " • From Group: ${ entry.groupName} "
153+ println " • Group Path: ${ entry.path} "
154+ println " • Context: ${ entry.context} "
155+ println " • Permissions:"
156+ entry. permissions. each { println " - ${ it} " }
157+ println " "
158+ }
159+ }
160+
161+ def checkRoot (username ) {
162+ def config = RoleMatrixAuthorizationPlugin . getConfig()
163+ def allGroups = config. getGroups()
164+
165+ // Build lookup maps
166+ def groupMap = allGroups. collectEntries { [(it. name): it] }
167+ def parentMap = [:]. withDefault { [] }
168+ allGroups. each { group ->
169+ group. getGroups(). each { nested ->
170+ parentMap[nested] << group. name
171+ }
172+ }
173+
174+ // Find all directly assigned groups
175+ def userGroups = allGroups. findAll { g ->
176+ g. metaClass. respondsTo(g, " getUsers" ) && g. getUsers(). contains(username)
177+ }
178+
179+ if (userGroups. isEmpty()) {
180+ println " ❌ User '${ username} ' is not a member of any configured group."
181+ return
182+ }
183+
184+ // Traverse group ancestry and record roles and permissions
185+ def visitedPaths = [] as Set
186+ def roleDetails = [] // list of [roleName, groupName, fullPath, permissionList]
187+ def roleNameDetails = []
188+
189+ userGroups. each { group ->
190+ def start = group. name
191+ def stack = [[start]]
192+
193+ while (! stack. isEmpty()) {
194+ def path = stack. pop()
195+ def currentGroupName = path[-1 ]
196+
197+ visitedPaths << path
198+
199+ parentMap[currentGroupName]. each { parent ->
200+ if (! path. contains(parent)) {
201+ stack. push(path + [parent])
202+ }
203+ }
204+ }
205+ }
206+
207+ // Collect roles + permissions per path
208+ visitedPaths. each { path ->
209+ def reversed = path. reverse() // from top-level → direct group
210+ reversed. each { groupName ->
211+ def group = groupMap[groupName]
212+ if (group) {
213+ def roles = group. getAllRoles()
214+ roles. each { roleName ->
215+ try {
216+ if (! roleNameDetails. contains(roleName)){
217+ def role = new Role (roleName)
218+ def perms = role. getPermissionProxies(). collect { " ${ it.group.title} / ${ it.name} " }
219+ roleDetails << [
220+ roleName : roleName,
221+ fromGroup : groupName,
222+ fullPath : reversed. join(" → " ),
223+ permissions : perms
224+ ]
225+ roleNameDetails << roleName
226+ }
227+ } catch (Exception e) {
228+ println " ⚠️ Could not resolve role '${ roleName} ': ${ e.message} "
229+ }
230+ }
231+ }
232+ }
233+ }
234+
235+ // Print summary
236+ roleDetails. each { entry ->
237+ println " 🔹 Role: ${ entry.roleName} "
238+ println " • From Group: ${ entry.fromGroup} "
239+ println " • Group Path: ${ entry.fullPath} "
240+ println " • Context: Jenkins"
241+ println " • Permissions:"
242+ entry. permissions. each { println " - ${ it} " }
243+ println " "
244+ }
245+ }
246+
247+ println " Gathering RBAC roles for user '${ username} ' in item '${ itemPath} ' and all inherited scopes..."
248+
249+ if (itemPath!= null && ! itemPath. isEmpty() && ! itemPath. equals(" /" )){
250+ checkingItems(username, itemPath)
251+ }
252+ checkRoot(username)
253+
254+ return
0 commit comments