diff --git a/enable-and-configure-rbac.groovy b/enable-and-configure-rbac.groovy new file mode 100644 index 0000000..1476123 --- /dev/null +++ b/enable-and-configure-rbac.groovy @@ -0,0 +1,328 @@ +/** +Author: philbert +Since: December 2019 +Description: Enable and configure RBAC roles, permissions and root groups in Cloudbees Core +Requirements: + file /tmp/rbac_config.yaml (see included example) +Scope: Cloudbees Jenkins Operations Center +Tested on: CloudBees Jenkins Operations Center 2.190.2.2-rolling, nectar-rbac 5.25 +**/ + +import jenkins.model.Jenkins +import hudson.security.Permission +import nectar.plugins.rbac.strategy.RoleMatrixAuthorizationStrategyImpl +import nectar.plugins.rbac.strategy.RoleMatrixAuthorizationConfig +import nectar.plugins.rbac.strategy.RoleMatrixAuthorizationPlugin +import nectar.plugins.rbac.groups.Group +import nectar.plugins.rbac.groups.GroupContainer +import nectar.plugins.rbac.groups.GroupContainerLocator +import nectar.plugins.rbac.groups.RootProxyGroupContainer +import nectar.plugins.rbac.roles.Role +import nectar.plugins.rbac.roles.Roles +import java.io.IOException +import org.yaml.snakeyaml.Yaml + +// Recommended to download the javadocs as explained here +// https://support.cloudbees.com/hc/en-us/articles/228175367-Custom-Plugins-APIs-and-Javadocs-of-CloudBees-Jenkins-Enterprise-plugins +// than use this method, but it's still here just in case +void printAllMethods( obj ){ + if( !obj ){ + println( "Object is null\r\n" ); + return; + } + if( !obj.metaClass && obj.getClass() ){ + printAllMethods( obj.getClass() ); + return; + } + def str = "class ${obj.getClass().name} functions:\r\n"; + obj.metaClass.methods.name.unique().each{ + str += it+"(); "; + } + println "${str}\r\n"; +} + +// simple wrapper around addRole() that returns the Role object that was just created +Role createRole(String roleName) { + RoleMatrixAuthorizationStrategyImpl strategy = RoleMatrixAuthorizationStrategyImpl.getInstance() + strategy.addRole( roleName ) + return getRoleByName( roleName ) +} + +// A wrapper around removeRole that attempts to remove all the permissions and delete the role +// This will not succeed if the role is still associated to any groups +void deleteRole(String roleName) { + RoleMatrixAuthorizationStrategyImpl strategy = RoleMatrixAuthorizationStrategyImpl.getInstance() + Role roleToDelete = getRoleByName(roleName) + List permissionsToRemove = roleToDelete.getPermissionProxyIds() + permissionsToRemove.each { + roleToDelete.doRevokePermissions(it) + } + strategy.removeRole( roleToDelete.getDisplayName() ) +} + +// Permissions change depending on the plugins that are installed not just in the CJOC +// but also the Managed Masters. I think this is what are called "proxy permissions". +// Since the permissions will change over time, we need a dynamic way to list all of them +List listAllPermissions() { + List validPermissions = [] + + // We must add the role to the strategy in order to discover all the permissions + Role tempRole = createRole('tempRole') + // getAll() does not include proxy permission + for (Permission p: Permission.getAll()) { + try { + p.setEnabled(true) + tempRole.doGrantPermissions(p.id) + validPermissions.add(p.id) + } catch (IOException message) { + println 'ignoring ' + p.id + // at the time of writing these seem to be all the ignored permissions + // hudson.security.Permission.FullControl + // hudson.security.Permission.GenericRead + // hudson.security.Permission.GenericWrite + // hudson.security.Permission.GenericCreate + // hudson.security.Permission.GenericUpdate + // hudson.security.Permission.GenericDelete + // hudson.security.Permission.GenericConfigure + } + } + + // Likely these permissions are from plugins in Managed Masters + tempRole.getPermissionProxyIds().each { r -> + tempRole.doGrantPermissions(r) + validPermissions.add(r) + //println 'added ' +r + } + + deleteRole( tempRole.getDisplayName() ) + + return validPermissions.unique() +} + +// We need to be careful about how we change and update the group permissions. +// At the time of writing this code only expects a group called 'administrators' +// to be present in the rbac_config.yaml - meaning that all other groups must be +// created and updated by the 'onboarding' code +Map validateRootGroups(Map rootGroups) { + List groupsFromConfig = rootGroups.keySet() as List + boolean valid = true + + // Validate groups only contain administrators + if (groupsFromConfig.size() != 1 ) { + println 'some other group configuration is present' + valid = false + } + + if (!groupsFromConfig.contains() == 'administrators') { + println 'root_groups config does not contain administrators group' + valid = false + } + + if (!valid) { + throw new IOException() + } +} + +// We attempt to validate the permissions in the role_config look correct to the best +// of our knowledge, and we will throw an IOException if anything looks fishy +Map validateRolePermisisons(Map rolePermissions) { + List rolesFromConfig = rolePermissions.keySet() as List + boolean valid = true + + // Validate role_permissions + rolesFromConfig.each { + Map keyValidation = rolePermissions[it] + List validKey = keyValidation.keySet() as List + + if (validKey.size() != 1) { + println 'something else than permissions found?' + validKey + valid = false + } + if (validKey[0] != 'permissions') { + println 'invalid key = ' + validKey[0] + valid = false + } + } + + if (!valid) { + throw new IOException() + } +} + +// a wrapper around the methods validateRolePermissions and validateRootGroups +Map validateRbacConfig(configFile) { + Yaml parser = new Yaml() + Map role_config = [:] + Map root_groups = [:] + Map rbac_config = parser.load((configFile as File).text) + if (rbac_config.containsKey('role_config')) { + validateRolePermisisons(rbac_config.role_config) + } + else { + throw new IOException() + } + + if (rbac_config.containsKey('root_groups')) { + validateRootGroups(rbac_config.root_groups) + } + else { + throw new IOException() + } + + return rbac_config +} + +// return the Role object of an existing role from it's name +Role getRoleByName( String roleName ) { + List allRoles = new Roles().getRoles() + Role roleToReturn = null + allRoles.each { + if (roleName == it.getDisplayName()) { + roleToReturn = it + } + } + return roleToReturn +} + +// Attempt to create any new roles in the role_config and remove any that are not in the role_config +void addOrRemoveRoles(List rolesFromConfig) { + List existingRoleNames = [] + List rolesToRemove = [] + List rolesToAdd = [] + + // get all the existing roles + List allRoles = new Roles().getRoles() + allRoles.each { + existingRoleNames.add(it.getDisplayName()) + } + // add any new roles in the config file + rolesToAdd = rolesFromConfig - existingRoleNames + println 'rolesToAdd = ' + rolesToAdd + rolesToAdd.each { + createRole(it) + } + // delete any roles not in the config file + rolesToRemove = existingRoleNames - rolesFromConfig + println 'rolesToRemove = ' + rolesToRemove + rolesToRemove.each { + deleteRole(it) + } +} + +// Add any permissions that exist in the role_config and remove any permissions not included in the role_config. +// There are two magic keywords "all" and "none" to make things slightly easier for admins. +void applyRolesAndPermissions(Map rbac_config) { + List allPermissions = listAllPermissions() + println "allPermissions: " + allPermissions.each { println it } + + List rolesFromConfig = rbac_config.role_config.keySet() as List + + // update our roles before assigning permissions + addOrRemoveRoles(rolesFromConfig) + + rolesFromConfig.each{ + println 'role: ' + it + List permissionsInRole = rbac_config.role_config[it].permissions + // 'none' has top priority in the list + if (permissionsInRole.contains('none')) { + Role noPermsRole = getRoleByName(it) + allPermissions.each { + noPermsRole.doRevokePermissions(it) + } + println ' remove all permissions' + } + // 'all' has second highest priority in the list + else if (permissionsInRole.contains('all')) { + Role allPermsRole = getRoleByName(it) + println ' add all permissions' + allPermissions.each { + try { + allPermsRole.doGrantPermissions(it) + } catch (IOException message) { + println ' ignoring permission ' + it + } + } + } + else { + List removePermissions = allPermissions - permissionsInRole + Role modifyPermsRole = getRoleByName(it) + removePermissions.each { + modifyPermsRole.doRevokePermissions(it) + println ' removed permission: ' + it + } + permissionsInRole.each { + try { + modifyPermsRole.doGrantPermissions(it) + println ' granted permission: ' + it + } catch (IOException message) { + println ' ignoring permission ' + it + } + } + } + } +} + +// Root groups exist only on the cjoc level, and this code is only concerned with who should have admin on CJOC. +void applyRootGroups(Map rbac_config) { + + RoleMatrixAuthorizationConfig config = RoleMatrixAuthorizationPlugin.getConfig() + RootProxyGroupContainer cjocRootContainer = RoleMatrixAuthorizationPlugin.getInstance().getRootProxyGroupContainer() + List groupsFromConfig = rbac_config.root_groups.keySet() as List + + List existingRootGroups = config.getGroups() + List updatedRootGroups = new ArrayList() + String adminGroupName = 'administrators' + Group adminGroup = new Group(adminGroupName) + + for ( Group group : existingRootGroups ) { + if ( adminGroupName != group.getName() ) { + println "Existing non-admin group found: " + group + updatedRootGroups.add(group) + } + } + + // create the new administrators group + List adminMembers = new ArrayList() + groupsFromConfig.each { + // any extra validation needed? + rbac_config.root_groups[it].each { + adminMembers.add(it) + } + } + // this will overwrite any axisting members! + adminGroup.setMembers(adminMembers) + + updatedRootGroups.add(adminGroup) + + // This will overwrite the existing groups, so it must be a complete set + config.setGroups(updatedRootGroups) + + // add role to group + adminGroup.doGrantRole('cjoc_admin', 0, true) + + // set the top-level cjoc for administrators + //locationFolder = Jenkins.instance.getItemByFullName('') + GroupContainer container = GroupContainerLocator.locate(cjocRootContainer) + container.addGroup(adminGroup) +} + +void doit() { + // enable RBAC + RoleMatrixAuthorizationStrategyImpl rbac = new RoleMatrixAuthorizationStrategyImpl(); + Jenkins.instance.setAuthorizationStrategy(rbac) + + // check that yaml looks ok + Map rbac_config = validateRbacConfig('/tmp/rbac_config.yaml' ) + + // The groups applied in this rbac_config will overwrite the existing configuration! + // It will not change any other groups + // Do this first before modifying the roles and permissions! + applyRootGroups(rbac_config) + + // add or remove all roles and permissions according to what is in the config + applyRolesAndPermissions(rbac_config) +} + +doit() + diff --git a/rbac_config.yaml b/rbac_config.yaml new file mode 100644 index 0000000..3c5417c --- /dev/null +++ b/rbac_config.yaml @@ -0,0 +1,132 @@ +--- +root_groups: +# Whatever groups are listed here should be separate from the groups configured in the team onboarding yaml descriptor +# Some care must be taken when overwriting the groups so that we don't get locked out of the system. + administrators: + - admin + +# This config not only grants permissions, it will also revoke them. +# The way this must occur is as follows: +# 1. Build a list of all PermissionIds and PermissionProxyIds +# 2. Subtract the list rbac_config..permissions from the list of all permissions. +# 3. Remove permissions from the list in #2 +# 4. Add permissions from rbac_config..permissions +# +# Note: "all" and "none" are magical keywords handled by enable-and-configure-rbac.groovy +# Except for the cjoc_admin role, the sets of permissions are unvalidated best guesses. +# This file also will likely need to include configuration for filerable: true/false +role_config: + cjoc_admin: # This is the global admin role + permissions: + - all + mm_admin: # Intended for individuals allowed to manage the config of masters + permissions: + - com.cloudbees.opscenter.server.model.ClientMaster.Configure + - com.cloudbees.opscenter.server.model.ClientMaster.Lifecycle + - com.cloudbees.plugins.credentials.CredentialsProvider.UseItem + - com.cloudbees.plugins.credentials.CredentialsProvider.UseOwn + - hudson.model.Hudson.Read + - hudson.model.Item.Build + - hudson.model.Item.Cancel + - hudson.model.Item.Configure + - hudson.model.Item.Create + - hudson.model.Item.Delete + - hudson.model.Item.Discover + - hudson.model.Item.ExtendedRead + - hudson.model.Item.Move + - hudson.model.Item.Promote + - hudson.model.Item.Read + - hudson.model.Item.Request + - hudson.model.Item.WipeOut + - hudson.model.Item.Workspace + - hudson.model.Run.Artifacts + - hudson.model.Run.Delete + - hudson.model.Run.Update + - hudson.model.View.Configure + - hudson.model.View.Create + - hudson.model.View.Delete + - hudson.model.View.Read + - hudson.scm.SCM.Tag + power_user: # can create and configure jobs and generally make a mess of things + permissions: + - jenkins.metrics.api.Metrics.View + - com.cloudbees.plugins.credentials.CredentialsProvider.UseItem + - com.cloudbees.plugins.credentials.CredentialsProvider.Create + - com.cloudbees.plugins.credentials.CredentialsProvider.Update + - com.cloudbees.plugins.credentials.CredentialsProvider.View + - com.cloudbees.plugins.credentials.CredentialsProvider.Delete + - hudson.model.Hudson.Read + - hudson.model.Item.Build + - hudson.model.Item.Cancel + - hudson.model.Item.Configure + - hudson.model.Item.Create + - hudson.model.Item.Delete + - hudson.model.Item.Discover + - hudson.model.Item.ExtendedRead + - hudson.model.Item.Move + - hudson.model.Item.Promote + - hudson.model.Item.Read + - hudson.model.Item.Request + - hudson.model.Item.WipeOut + - hudson.model.Item.Workspace + - hudson.model.Run.Artifacts + - hudson.model.Run.Delete + - hudson.model.Run.Update + - hudson.model.View.Configure + - hudson.model.View.Create + - hudson.model.View.Delete + - hudson.model.View.Read + - hudson.scm.SCM.Tag + regular_user: # can run jobs but not create or configure + permissions: + - hudson.model.Hudson.Read + - hudson.model.Item.Build + - hudson.model.Item.Cancel + - hudson.model.Item.Configure + - hudson.model.Item.Create + - hudson.model.Item.Delete + - hudson.model.Item.Discover + - hudson.model.Item.ExtendedRead + - hudson.model.Item.Move + - hudson.model.Item.Promote + - hudson.model.Item.Read + - hudson.model.Item.Request + - hudson.model.Item.WipeOut + - hudson.model.Item.Workspace + - hudson.model.Run.Artifacts + - hudson.model.Run.Delete + - hudson.model.Run.Update + - hudson.model.View.Configure + - hudson.model.View.Create + - hudson.model.View.Delete + - hudson.model.View.Read + - hudson.scm.SCM.Tag + anonymous: # anonymous is a default role. Maybe should be removed? + permissions: + - none + authenticated: # authenticated is a default role + permissions: # There seems to be a bug with this role that leave many permissions + - hudson.model.Item.Discover # in place. If you disable filterable on the role it seems to get + - hudson.model.Item.Read # the correct permissions + - hudson.model.View.Read +# auditor: # for security? +# permissions: +# - all the "read only" permissions +# +# Validation of this yaml file is quite strict. +# All the declarations below will throw an exception +# foo: # invalid +# permissions: 1234 +# bar: # invalid +# permissions: 'some_error' +# baz: # invalid +# permissions: true +# wuz: # invalid +# something: ['blah'] +# fuz: # invalid +# permissions: +# yo: bro +# woz: # invalid +# permissions: ['valid_entry'] +# something_else: ['w00t'] +