From a00365519c281d1739fd96e1fb330f9d0aae4f7b Mon Sep 17 00:00:00 2001 From: Stefan Profanter Date: Fri, 22 Apr 2022 12:07:18 +0200 Subject: [PATCH] feat: implement ipWhitelistUserLogin Plugin --- security/ipWhitelistUserLogin/README.md | 40 +++++++ .../ipWhitelistUserLogin.groovy | 75 +++++++++++++ .../ipWhitelistUserLogin.json | 12 +++ .../ipWhitelistUserLoginTest.groovy | 102 ++++++++++++++++++ 4 files changed, 229 insertions(+) create mode 100644 security/ipWhitelistUserLogin/README.md create mode 100644 security/ipWhitelistUserLogin/ipWhitelistUserLogin.groovy create mode 100644 security/ipWhitelistUserLogin/ipWhitelistUserLogin.json create mode 100644 security/ipWhitelistUserLogin/ipWhitelistUserLoginTest.groovy diff --git a/security/ipWhitelistUserLogin/README.md b/security/ipWhitelistUserLogin/README.md new file mode 100644 index 00000000..38d120b7 --- /dev/null +++ b/security/ipWhitelistUserLogin/README.md @@ -0,0 +1,40 @@ +Artifactory IP-based User Login Whitelisting +===================================================== + +This plugin allows to limit user login for specific users to a specific list of IP addresses. + +1. If a user is not listed in the configuration, the login is not restricted, and it is the default as it would be without using this plugin +2. The configuration allows to specify for a specific username the allowed IP address and the Group to be assigned when logging in. +3. If a user is listed in the configuration, all given IP addresses are checked. This is a simple 'starts With' check. + 1. If the IP address is not included in the list, the User Login is prevented + 2. If the IP address is in the list, the User gets assigned the given Group (if it exists), and can log in + +## Configuration + +The configuration [ipWhitelistUserLogin.json](ipWhitelistUserLogin.json) is used to configure +the plugin. + +The `assignGroup` option is optional, but should be used for security reasons! + +I.e., use this specific group to define permissions for the user, do not use the username directly. +If the plugin is somehow disabled for whatever reason, the user may log in from any IP and automatically has +the given rights. +If you use a dedicated group, and this Plugin is somehow disabled, the user is not assigned to the group, +and therefore does not have any rights. + + +Based on the idea given here: +https://www.jfrog.com/jira/browse/RTFACT-9157 + +Installation +------------ + +To install this plugin: + +1. Place the configuration file + `ipWhitelistUserLogin.json` file in the + `${ARTIFACTORY_HOME}/etc/plugins` directory and adapt it to your needs. +2. Create any groups you configure in the config, and adapt the permissions. +3. Place the `ipWhitelistUserLogin.groovy` file in the + `${ARTIFACTORY_HOME}/etc/plugins` directory. +4. You are done. Verify that the plugin properly works by trying to log in from a non-allowed IP. \ No newline at end of file diff --git a/security/ipWhitelistUserLogin/ipWhitelistUserLogin.groovy b/security/ipWhitelistUserLogin/ipWhitelistUserLogin.groovy new file mode 100644 index 00000000..0cd3a087 --- /dev/null +++ b/security/ipWhitelistUserLogin/ipWhitelistUserLogin.groovy @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2014 JFrog Ltd. + * + * Licensed 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. + */ + +/** + * + * @author Stefan Profanter + * @since 21/04/22 + */ +import org.artifactory.security.RealmPolicy + +import groovy.json.JsonSlurper +import groovy.transform.Field + + +@Field final String CONFIG_FILE_PATH = "plugins/ipWhitelistUserLogin.json" +def configFile = new File(ctx.artifactoryHome.etcDir, CONFIG_FILE_PATH) + +def config = null + +if ( configFile.exists() ) { + + config = new JsonSlurper().parse(configFile.toURL()) + log.info "Loaded ipWhitelistUserLogin config for users: $config.users" + +} else { + log.error "Config file $configFile is missing!" +} + + +realms { + ipWhitelistRealm(realmPolicy: RealmPolicy.ADDITIVE) { + authenticate { username, credentials -> + if (config == null) { + log.error "Config file $configFile is missing!" + return true + } + + String ip = request.getClientAddress() + + if (!config.users.containsKey(username)) { + return true + } + + for (allow_ip in config.users[username]['allow']) { + if (ip.startsWith("$allow_ip")){ + if (config.users[username].containsKey("assignGroup")) { + String groupName = config.users[username]['assignGroup'] + log.info "${username} is trying to login from whitelisted IP, assigning the user to ${groupName}" + groups += groupName + } else { + log.info "User: ${username} with IP ${ip} matches allowed IP ${allow_ip}" + } + return true + } + } + + log.warn "User '${username}' login not allowed. IP address ${ip} not whitelisted" + return false + } + } +} + diff --git a/security/ipWhitelistUserLogin/ipWhitelistUserLogin.json b/security/ipWhitelistUserLogin/ipWhitelistUserLogin.json new file mode 100644 index 00000000..451f10e6 --- /dev/null +++ b/security/ipWhitelistUserLogin/ipWhitelistUserLogin.json @@ -0,0 +1,12 @@ +{ + "users": { + "alice": { + "allow": ["172.21.0.", "127.0.0."], + "assignGroup": "ip-allowed" + }, + "bob": { + "allow": ["172.21.0.", "127.0.0."], + "assignGroup": "ip-allowed-bob" + } + } +} \ No newline at end of file diff --git a/security/ipWhitelistUserLogin/ipWhitelistUserLoginTest.groovy b/security/ipWhitelistUserLogin/ipWhitelistUserLoginTest.groovy new file mode 100644 index 00000000..ce773fe8 --- /dev/null +++ b/security/ipWhitelistUserLogin/ipWhitelistUserLoginTest.groovy @@ -0,0 +1,102 @@ +import org.artifactory.api.repo.storage.RepoStorageSummaryInfo +import org.jfrog.artifactory.client.Artifactory +import org.jfrog.artifactory.client.RepositoryHandle +import org.jfrog.artifactory.client.model.LightweightRepository +import org.jfrog.artifactory.client.model.LocalRepository +import org.jfrog.artifactory.client.model.PackageType +import org.jfrog.artifactory.client.model.Privilege +import org.jfrog.artifactory.client.model.Repository +import org.jfrog.artifactory.client.model.RepositoryType +import org.jfrog.artifactory.client.model.builder.GroupBuilder +import org.jfrog.artifactory.client.model.builder.LocalRepositoryBuilder +import org.jfrog.artifactory.client.model.repository.settings.ConanRepositorySettings +import org.jfrog.artifactory.client.model.repository.settings.RepositorySettings +import org.jfrog.artifactory.client.model.repository.settings.impl.ConanRepositorySettingsImpl +import org.jfrog.artifactory.client.model.repository.settings.impl.MavenRepositorySettingsImpl +import spock.lang.Specification + +import org.jfrog.artifactory.client.ArtifactoryClientBuilder +import org.jfrog.artifactory.client.model.builder.UserBuilder + +class IpWhitelistUserLoginTest extends Specification { + + def grantRepoReadPermissionToGroup (Artifactory artifactory, String permissionName, String groupName, String repoKey) { + def principal = artifactory.security().builders().principalBuilder() + .name(groupName) + .privileges(Privilege.READ) + .build() + def principals = artifactory.security().builders().principalsBuilder() + .groups(principal) + .build() + def permission = artifactory.security().builders().permissionTargetBuilder() + .name(permissionName) + .repositories(repoKey) + .principals(principals) + .build() + artifactory.security().createOrReplacePermissionTarget(permission) + } + + def 'ip whitelist user login test'() { + setup: + def baseurl1 = 'http://localhost:8082/artifactory' + def password = "password" + def artifactory = ArtifactoryClientBuilder.create().setUrl(baseurl1).setUsername('admin').setPassword(password).build() + def artifactory_bob = ArtifactoryClientBuilder.create().setUrl(baseurl1).setUsername('bob').setPassword(password).build() + + when: + + /* + + 1. Create a group (same as in the config, which will be assigned by the plugin) + 2. Create a repository: conan-test + 3. Allow the group to read that repository + 4. Create the user `bob` (without assigning the group) + 5. Then test that user `bob` gets the group assigned by the plugin, by verifying that it can see the repo + + */ + + GroupBuilder groupBilder = artifactory.security().builders().groupBuilder() + def ip_allowed__group = groupBilder + .name("ip-allowed-bob") + .adminPrivileges(false) + .autoJoin(false).build(); + artifactory.security().createOrUpdateGroup(ip_allowed__group) + + LocalRepositoryBuilder localRepositoryBuilder = artifactory.repositories().builders().localRepositoryBuilder() + localRepositoryBuilder.key("conan-test").repositorySettings(new ConanRepositorySettingsImpl()) + artifactory.repositories().create(0, localRepositoryBuilder.build()) + artifactory.security().builders().permissionTargetBuilder() + + grantRepoReadPermissionToGroup(artifactory, "conan-test-read", "ip-allowed-bob", "conan-test") + + UserBuilder userBuilder = artifactory.security().builders().userBuilder() + def user1 = userBuilder.name("bob") + .email("newuser@jfrog.com") + .admin(false) + .profileUpdatable(false) + .password(password) + .build(); + artifactory.security().createOrUpdate(user1) + + then: + + def repos_list = artifactory_bob.repositories().list() + + boolean repo_found = false + + for (repo in repos_list) { + if (repo.key == "conan-test") { + repo_found = true + break + } + } + + repo_found + + cleanup: + String result3 = artifactory.repository("conan-test").delete() + String result2 = artifactory.security().deleteGroup("ip-allowed-bob") + String result4 = artifactory.security().deletePermissionTarget("conan-test-read") + String result1 = artifactory.security().deleteUser("bob") + } +}