├── circuitBreaker.txt ├── src ├── main │ ├── resources │ │ ├── org │ │ │ └── jenkinsci │ │ │ │ └── account │ │ │ │ ├── taglib │ │ │ │ ├── taglib │ │ │ │ └── layout.jelly │ │ │ │ ├── Application │ │ │ │ ├── done.jelly │ │ │ │ ├── resetMail.jelly │ │ │ │ ├── doneMail.jelly │ │ │ │ ├── index.jelly │ │ │ │ ├── passwordReset.jelly │ │ │ │ ├── login.jelly │ │ │ │ └── signup.jelly │ │ │ │ ├── Myself │ │ │ │ ├── done.jelly │ │ │ │ └── index.jelly │ │ │ │ ├── AdminUI │ │ │ │ ├── newPassword.jelly │ │ │ │ ├── signup.jelly │ │ │ │ ├── index.jelly │ │ │ │ └── search.jelly │ │ │ │ ├── SystemError │ │ │ │ └── index.jelly │ │ │ │ ├── openid │ │ │ │ └── JenkinsOpenIDSession │ │ │ │ │ ├── confirm.jelly │ │ │ │ │ └── index.jelly │ │ │ │ └── UserError │ │ │ │ └── index.jelly │ │ └── application.conf │ ├── java │ │ ├── org │ │ │ └── jenkinsci │ │ │ │ └── account │ │ │ │ ├── LdapAbuse.java │ │ │ │ ├── openid │ │ │ │ ├── JenkinsOpenIDServer.java │ │ │ │ └── JenkinsOpenIDSession.java │ │ │ │ ├── SystemError.java │ │ │ │ ├── config │ │ │ │ ├── LdapConfig.java │ │ │ │ └── MailConfig.java │ │ │ │ ├── UserError.java │ │ │ │ ├── PasswordUtil.java │ │ │ │ ├── WebAppMain.java │ │ │ │ ├── CircuitBreaker.java │ │ │ │ ├── Parameters.java │ │ │ │ ├── batch │ │ │ │ └── UpdateSeniorGroup.java │ │ │ │ ├── AdminUI.java │ │ │ │ ├── Myself.java │ │ │ │ └── Application.java │ │ └── BulkImport.java │ └── webapp │ │ ├── WEB-INF │ │ └── web.xml │ │ └── style.css ├── it │ ├── java │ │ └── org │ │ │ └── jenkinsci │ │ │ └── account │ │ │ └── ui │ │ │ ├── resetpassword │ │ │ ├── UserLookupType.java │ │ │ ├── ResetPasswordPage.java │ │ │ └── ResetPasswordTest.java │ │ │ ├── email │ │ │ ├── Emails.java │ │ │ └── ReadInboundEmailService.java │ │ │ ├── login │ │ │ ├── LoginTest.java │ │ │ └── LoginPage.java │ │ │ ├── admin │ │ │ ├── AdminResetPasswordResultPage.java │ │ │ ├── AdminPage.java │ │ │ ├── AdminSearchPage.java │ │ │ ├── DeleteUserTest.java │ │ │ └── ResetPasswordAdminTest.java │ │ │ ├── myaccount │ │ │ ├── MyAccountPage.java │ │ │ ├── MyProfilePage.java │ │ │ └── UpdateMyAccountTest.java │ │ │ └── BaseTest.java │ └── resources │ │ └── org │ │ └── jenkinsci │ │ └── account │ │ └── ui │ │ └── test-data.ldif └── test │ └── java │ └── org │ └── jenkinsci │ └── account │ └── Foo.java ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── 3-documentation.yml │ ├── 2-enhancement.yml │ └── 1-bug.yml ├── release-drafter.yml ├── workflows │ └── release-drafter.yml └── renovate.json ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── bulk-import.sh ├── .gitignore ├── settings.gradle.kts ├── LICENSE ├── entrypoint.sh ├── Jenkinsfile ├── README.md ├── Dockerfile ├── mock-ldap └── data.ldif ├── docker-compose.yaml ├── gradlew.bat └── gradlew /circuitBreaker.txt: -------------------------------------------------------------------------------- 1 | Disabled sign-up temporarily -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/account/taglib/taglib: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: true 2 | -------------------------------------------------------------------------------- /.github/release-drafter.yml: -------------------------------------------------------------------------------- 1 | _extends: .github 2 | 3 | name-template: 'next' 4 | tag-template: 'next' 5 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jenkins-infra/account-app/main/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /bulk-import.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | exec mvn -e compile exec:java -Dexec.mainClass=BulkImport "-Dexec.args=$1" -Dexec.classpathScope=test 3 | -------------------------------------------------------------------------------- /src/it/java/org/jenkinsci/account/ui/resetpassword/UserLookupType.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.account.ui.resetpassword; 2 | 3 | public enum UserLookupType { 4 | EMAIL, USERNAME 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.ipr 3 | *.iws 4 | target 5 | config.properties 6 | bin 7 | .idea 8 | .gradle/ 9 | *.sw* 10 | build/ 11 | .env 12 | fake_data 13 | 14 | errorScreenshots/ 15 | 16 | # IDE 17 | .classpath 18 | .project 19 | .settings 20 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/account/Application/done.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Done!

5 |
6 |
7 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/account/Myself/done.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Done!

5 |
6 |
7 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/account/AdminUI/newPassword.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

5 | The password of ${user.id} (${user.mail}) is reset to: ${password} 6 |

7 |
8 |
9 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/account/SystemError/index.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Oops!

5 |

6 |
7 |
8 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "accountapp" 2 | 3 | // https://github.com/dom4j/dom4j/pull/116#issuecomment-770092526 4 | dependencyResolutionManagement { 5 | components { 6 | withModule("org.dom4j:dom4j") { 7 | allVariants { 8 | withDependencies { 9 | clear() 10 | } 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/it/java/org/jenkinsci/account/ui/email/Emails.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.account.ui.email; 2 | 3 | import java.util.regex.Pattern; 4 | 5 | public class Emails { 6 | 7 | public static final Pattern PASSWORD_EXTRACTOR = Pattern.compile("Your temporary password is ([a-zA-Z0-9]+)"); 8 | 9 | public static final String RESET_PASSWORD_SUBJECT = "Password reset on the Jenkins project infrastructure"; 10 | } 11 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/account/Application/resetMail.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Done!

5 |

6 | If your user account or email address exists in our database, you will receive a password at your email address in a few minutes 7 |

8 |
9 |
-------------------------------------------------------------------------------- /src/test/java/org/jenkinsci/account/Foo.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.account; 2 | 3 | /** 4 | * @author Kohsuke Kawaguchi 5 | */ 6 | public class Foo { 7 | public static void main(String[] args) throws Exception { 8 | Application a = new WebAppMain().createApplication(); 9 | String kohsuke = "cn=kohsuke,ou=people,dc=jenkins-ci,dc=org"; 10 | System.out.println(a.getGroups(kohsuke, a.connect())); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/account/Application/doneMail.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Account created

5 |

6 | Check your email for the password. 7 | You can log in to Jira and Artifactory, 8 | with this username and password. 9 |

10 |
11 |
12 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/account/openid/JenkinsOpenIDSession/confirm.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | You are trying to login to ${it.realm} with your Jenkins ID ${it.identity.nick} 6 |
7 | 8 |
9 | 12 |
13 |
14 |
15 | -------------------------------------------------------------------------------- /src/it/java/org/jenkinsci/account/ui/login/LoginTest.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.account.ui.login; 2 | 3 | import org.jenkinsci.account.ui.BaseTest; 4 | import org.jenkinsci.account.ui.login.LoginPage; 5 | import org.junit.jupiter.api.Test; 6 | 7 | import static org.assertj.core.api.Assertions.assertThat; 8 | 9 | class LoginTest extends BaseTest { 10 | 11 | @Test 12 | void acceptsValidPassword() { 13 | openHomePage(); 14 | 15 | LoginPage loginPage = new LoginPage(driver); 16 | 17 | loginPage.login("alice", "password"); 18 | 19 | String pageTitle = driver.getTitle(); 20 | assertThat(pageTitle).contains("Account"); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/account/UserError/index.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

Oops!

6 |

${it.message}

7 | 8 | Please consider reporting this as an issue in the Jenkins Infrastructure help desk. 9 | 10 |
11 |
12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/3-documentation.yml: -------------------------------------------------------------------------------- 1 | name: '📝 Documentation' 2 | labels: 'documentation' 3 | description: 'Let us know if any documentation is missing or could be improved' 4 | 5 | body: 6 | - type: textarea 7 | attributes: 8 | id: suggestion 9 | label: Describe your use-case which is not covered by existing documentation. 10 | description: If it is easier to submit a documentation patch instead of writing an issue, just do it! 11 | validations: 12 | required: true 13 | - type: textarea 14 | attributes: 15 | label: Reference any relevant documentation, other materials or issues/pull requests that can be used for inspiration. 16 | validations: 17 | required: false 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-enhancement.yml: -------------------------------------------------------------------------------- 1 | name: '🚀 Website enhancement' 2 | labels: 'enhancement' 3 | description: 'I have a suggestion how to improve the website (and may want to implement it 🙂)!' 4 | 5 | body: 6 | - type: textarea 7 | attributes: 8 | id: suggestion 9 | label: Suggestion 10 | description: A clear and concise description of what the problem is. Ex. `I have an issue when [...]`, `As a Jenkins user, I want to [...] 11 | validations: 12 | required: true 13 | - type: textarea 14 | attributes: 15 | id: links 16 | label: Links 17 | description: Link any related documentation pages and other materials. 18 | placeholder: | 19 | * Links 1 20 | validations: 21 | required: false 22 | -------------------------------------------------------------------------------- /src/it/java/org/jenkinsci/account/ui/admin/AdminResetPasswordResultPage.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.account.ui.admin; 2 | 3 | import org.openqa.selenium.WebDriver; 4 | import org.openqa.selenium.WebElement; 5 | import org.openqa.selenium.support.FindBy; 6 | import org.openqa.selenium.support.PageFactory; 7 | import java.util.List; 8 | 9 | // page_url = http://localhost:8080/admin/passwordReset 10 | public class AdminResetPasswordResultPage { 11 | @FindBy(xpath = "//p/code") 12 | private WebElement newPasswordText; 13 | 14 | public AdminResetPasswordResultPage(WebDriver driver) { 15 | PageFactory.initElements(driver, this); 16 | } 17 | 18 | public String getNewPassword() { 19 | return newPasswordText.getText(); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.github/workflows/release-drafter.yml: -------------------------------------------------------------------------------- 1 | name: release-drafter 2 | on: 3 | push: 4 | workflow_dispatch: 5 | release: 6 | # Only allow 1 release-drafter build at a time to avoid creating multiple "next" releases 7 | concurrency: "release-drafter" 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check out 13 | uses: actions/checkout@1af3b93b6815bc44a9784bd300feb67ff0d1eeb3 # v6 14 | with: 15 | fetch-depth: 0 16 | - name: Release Drafter 17 | uses: release-drafter/release-drafter@b1476f6e6eb133afa41ed8589daba6dc69b4d3f5 # v6 18 | with: 19 | name: next 20 | tag: next 21 | version: next 22 | env: 23 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 24 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/account/LdapAbuse.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.account; 2 | 3 | /** 4 | * Because I don't know how to expand schemas of slapd, we hijack 5 | * existing fields and use them for different purposes. 6 | * 7 | * @author Kohsuke Kawaguchi 8 | */ 9 | public class LdapAbuse { 10 | public static final String GITHUB_ID = "employeeNumber"; 11 | public static final String SSH_KEYS = "preferredLanguage"; 12 | 13 | /** 14 | * "YYYY/MM/DD HH:MM:SS" that represents when the account was created. 15 | */ 16 | public static final String REGISTRATION_DATE = "carLicense"; 17 | 18 | /** 19 | * Represents the status of the seniority processing. 20 | * 'N' for newly created users who don't belong to the senior list. 21 | */ 22 | public static final String SENIOR_STATUS = "businessCategory"; 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/account/openid/JenkinsOpenIDServer.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.account.openid; 2 | 3 | import org.jenkinsci.account.Application; 4 | import org.kohsuke.stapler.openid.server.OpenIDServer; 5 | import org.kohsuke.stapler.openid.server.Session; 6 | 7 | import java.io.IOException; 8 | import java.net.URL; 9 | 10 | /** 11 | * OpenID server that allows users to use their Jenkins identity as an OpenID. 12 | * 13 | * @author Kohsuke Kawaguchi 14 | */ 15 | public class JenkinsOpenIDServer extends OpenIDServer { 16 | public final Application app; 17 | 18 | public JenkinsOpenIDServer(Application app) throws IOException { 19 | super(new URL(app.getUrl()+"openid/")); 20 | this.app = app; 21 | } 22 | 23 | @Override 24 | protected Session createSession() { 25 | return new JenkinsOpenIDSession(this); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/it/java/org/jenkinsci/account/ui/myaccount/MyAccountPage.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.account.ui.myaccount; 2 | 3 | import org.openqa.selenium.WebDriver; 4 | import org.openqa.selenium.WebElement; 5 | import org.openqa.selenium.support.FindBy; 6 | import org.openqa.selenium.support.PageFactory; 7 | import java.util.List; 8 | 9 | // page_url = http://localhost:8080/ 10 | public class MyAccountPage { 11 | @FindBy(xpath = "//a[@href=\"/admin\"]") 12 | private WebElement administerLink; 13 | 14 | @FindBy(xpath = "//a[@href=\"/myself\"]") 15 | private WebElement profileLink; 16 | 17 | public MyAccountPage(WebDriver driver) { 18 | PageFactory.initElements(driver, this); 19 | } 20 | 21 | public void clickAdminLink() { 22 | administerLink.click(); 23 | } 24 | 25 | public void clickProfileLink() { 26 | profileLink.click(); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/account/SystemError.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.account; 2 | import org.kohsuke.stapler.HttpResponse; 3 | import org.kohsuke.stapler.HttpResponses; 4 | import org.kohsuke.stapler.StaplerRequest; 5 | import org.kohsuke.stapler.StaplerResponse; 6 | 7 | import javax.servlet.ServletException; 8 | import java.io.IOException; 9 | 10 | /** 11 | * Created by Olivier Vernin on 11/10/17. 12 | * Indicate an error send from the system which contain trusted information (no xss vulnerability) 13 | * and therefor doesn't need to be escaped 14 | */ 15 | public class SystemError extends RuntimeException implements HttpResponse { 16 | public SystemError(String message){ 17 | super(message); 18 | } 19 | 20 | public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) throws IOException, ServletException { 21 | rsp.forward(this,"index",req); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/resources/application.conf: -------------------------------------------------------------------------------- 1 | # must end with / 2 | url = "http://localhost:8080/" 3 | url = ${?APP_URL} 4 | 5 | # see https://github.com/jenkins-infra/mock-ldap 6 | ldap { 7 | server = "ldap://localhost:1389" 8 | server = ${?LDAP_URL} 9 | 10 | managerDN = "cn=admin,dc=jenkins-ci,dc=org" 11 | managerDN = ${?LDAP_MANAGER_DN} 12 | 13 | managerPassword = s3cr3t 14 | managerPassword = ${?LDAP_PASSWORD} 15 | 16 | newUserBaseDN = "ou=people,dc=jenkins-ci,dc=org" 17 | newUserBaseDN = ${?LDAP_NEW_USER_BASE_DN} 18 | } 19 | 20 | mail { 21 | server = localhost 22 | server = ${?SMTP_SERVER} 23 | sender = "admin@jenkins-ci.org" 24 | sender = ${?SMTP_SENDER} 25 | port = 2525 26 | port = ${?SMTP_PORT} 27 | user = ${?SMTP_USER} 28 | password = ${?SMTP_PASSWORD} 29 | useAuth = false 30 | useAuth = ${?SMTP_AUTH} 31 | } 32 | 33 | circuitBreakerFile = ${?CIRCUIT_BREAKER_FILE} 34 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/account/openid/JenkinsOpenIDSession/index.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |

Jenkins OpenID Server

8 |

9 | This service allows you to use your Jenkins account as an OpenID. 10 | It is of the following format: 11 |

12 |
13 |
14 | ${it.identity.getOpenId(it.server)} 15 |
16 |
17 |
18 | 19 |
20 | 21 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/account/Application/index.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 |

Hey ${it.myself.firstName}

6 | 7 |

8 | You can create/manage your user account that you use for accessing 9 | Jira, 10 | Artifactory, 11 | VPN and other services within the Jenkins Infrastructure. 12 |

13 |
14 | 15 | 16 | 19 | 20 |
21 |
22 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/account/config/LdapConfig.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.account.config; 2 | 3 | public class LdapConfig { 4 | 5 | private final String server; 6 | private final String managerDn; 7 | private final String managerPassword; 8 | private final String newUserBaseDn; 9 | 10 | public LdapConfig(String server, String managerDn, String managerPassword, String newUserBaseDn) { 11 | this.server = server; 12 | this.managerDn = managerDn; 13 | this.managerPassword = managerPassword; 14 | this.newUserBaseDn = newUserBaseDn; 15 | } 16 | 17 | public String getNewUserBaseDn() { 18 | return newUserBaseDn; 19 | } 20 | 21 | public String getManagerDn() { 22 | return managerDn; 23 | } 24 | 25 | public String getManagerPassword() { 26 | return managerPassword; 27 | } 28 | 29 | public String getServer() { 30 | return server; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/account/Application/passwordReset.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Reset password

5 | 6 |
7 |
8 | 9 | 10 |
11 | 12 | 13 | 14 |

15 | If you can't figure this out, contact us to get your account recovered. 16 |

17 |
18 |
19 |
20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-bug.yml: -------------------------------------------------------------------------------- 1 | name: '🐛 Bug report' 2 | labels: 'bug' 3 | description: 'Create a bug report for the website' 4 | 5 | body: 6 | - type: input 7 | id: problem 8 | attributes: 9 | label: Problem with this page 10 | description: The page URL you encounter issues on. 11 | validations: 12 | required: true 13 | - type: textarea 14 | id: expected-behavior 15 | attributes: 16 | label: Expected behavior 17 | description: Describe what you expected to happen. 18 | validations: 19 | required: true 20 | - type: textarea 21 | id: actual-behavior 22 | attributes: 23 | label: Actual behavior 24 | description: Describe what actually happened. 25 | validations: 26 | required: true 27 | - type: textarea 28 | id: possible-solution 29 | attributes: 30 | label: Possible solution 31 | description: If you have suggestions on a fix for the bug, please describe it here. 32 | validations: 33 | required: false 34 | -------------------------------------------------------------------------------- /src/it/java/org/jenkinsci/account/ui/admin/AdminPage.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.account.ui.admin; 2 | 3 | import org.openqa.selenium.WebDriver; 4 | import org.openqa.selenium.WebElement; 5 | import org.openqa.selenium.support.FindBy; 6 | import org.openqa.selenium.support.PageFactory; 7 | import java.util.List; 8 | 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | 11 | // page_url = http://localhost:8080/admin/ 12 | public class AdminPage { 13 | @FindBy(name = "word") 14 | private WebElement searchInput; 15 | 16 | @FindBy(xpath = "//button[contains(text(), 'Search')]") 17 | private WebElement searchButton; 18 | private final WebDriver driver; 19 | 20 | public AdminPage(WebDriver driver) { 21 | this.driver = driver; 22 | PageFactory.initElements(driver, this); 23 | } 24 | 25 | public void verifyOnPage() { 26 | assertThat(driver.getTitle()) 27 | .contains("Admin"); 28 | } 29 | 30 | public void search(String query) { 31 | searchInput.sendKeys(query); 32 | searchButton.click(); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/it/java/org/jenkinsci/account/ui/resetpassword/ResetPasswordPage.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.account.ui.resetpassword; 2 | 3 | import org.openqa.selenium.By; 4 | import org.openqa.selenium.WebDriver; 5 | import org.openqa.selenium.WebElement; 6 | import org.openqa.selenium.support.FindBy; 7 | import org.openqa.selenium.support.PageFactory; 8 | import java.util.List; 9 | 10 | // page_url = http://localhost:8080/passwordReset 11 | public class ResetPasswordPage { 12 | @FindBy(name = "id") 13 | private WebElement usernameOrEmailInput; 14 | 15 | @FindBy(xpath = "//button") 16 | private WebElement resetPrimaryBlockButton; 17 | 18 | private final WebDriver driver; 19 | 20 | public ResetPasswordPage(WebDriver driver) { 21 | PageFactory.initElements(driver, this); 22 | this.driver = driver; 23 | } 24 | 25 | public void resetPassword(String usernameOrEmail) { 26 | usernameOrEmailInput.sendKeys(usernameOrEmail); 27 | resetPrimaryBlockButton.click(); 28 | } 29 | 30 | public String resultText() { 31 | return driver.findElement(By.cssSelector("p")).getText(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Jenkins Infrastructure Team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | Footer 23 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/account/Application/login.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Sign in

5 | 6 |
7 |
8 | 9 | 10 |
11 | 12 |
13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 |
21 | 22 | 27 |
28 |
29 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/account/config/MailConfig.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.account.config; 2 | 3 | public class MailConfig { 4 | 5 | private final String smtpServer; 6 | private final String smtpSender; 7 | private final String smtpUser; 8 | private final String smtpPassword; 9 | private final boolean smtpAuth; 10 | private final int smtpPort; 11 | 12 | public MailConfig(String smtpServer, String smtpSender, int smtpPort, String smtpUser, String smtpPassword, boolean smtpAuth) { 13 | this.smtpServer = smtpServer; 14 | this.smtpSender = smtpSender; 15 | this.smtpUser = smtpUser; 16 | this.smtpPassword = smtpPassword; 17 | this.smtpAuth = smtpAuth; 18 | this.smtpPort = smtpPort; 19 | } 20 | 21 | public String getSmtpServer() { 22 | return smtpServer; 23 | } 24 | 25 | public String getSmtpSender() { 26 | return smtpSender; 27 | } 28 | 29 | public String getSmtpUser() { 30 | return smtpUser; 31 | } 32 | 33 | public String getSmtpPassword() { 34 | return smtpPassword; 35 | } 36 | 37 | public boolean isSmtpAuth() { 38 | return smtpAuth; 39 | } 40 | 41 | public int getSmtpPort() { 42 | return smtpPort; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -ex 4 | 5 | CUSTOM_CACERT_FILE=/var/lib/jetty/resources/localcacerts 6 | CUSTOM_CACERT_PASS=changeit 7 | 8 | declare -a main_command 9 | 10 | main_command=("java" 11 | "-Dcom.sun.jndi.ldap.connect.pool=true" 12 | "-Dcom.sun.jndi.ldap.connect.pool.protocol='plain ssl'" 13 | "-Dcom.sun.jndi.ldap.connect.pool.maxsize=0" 14 | "-Dcom.sun.jndi.ldap.connect.pool.prefsize=10" 15 | "-Dcom.sun.jndi.ldap.connect.pool.timeout=180000" 16 | "-Dcom.sun.jndi.ldap.connect.timeout=1000" 17 | ) 18 | 19 | if [ -n "${DD_AGENT_SERVICE_HOST}" ] && [ -n "${DD_AGENT_SERVICE_PORT}" ] 20 | then 21 | main_command+=("-Ddd.agent.host=${DD_AGENT_SERVICE_HOST}" "-Ddd.agent.port=${DD_AGENT_SERVICE_PORT}" "-javaagent:/home/jetty/dd-java-agent.jar") 22 | fi 23 | 24 | if [ -f "${CUSTOM_CERT_FILE}" ] 25 | then 26 | if [ ! -f ${CUSTOM_CACERT_FILE} ] 27 | then 28 | keytool -import -trustcacerts -alias localcacerts -noprompt -storepass "${CUSTOM_CACERT_PASS}" -file "${CUSTOM_CERT_FILE}" -keystore "${CUSTOM_CACERT_FILE}" 29 | fi 30 | main_command+=("-Djavax.net.ssl.trustStore=${CUSTOM_CACERT_FILE}" "-Djavax.net.ssl.trustStorePassword=${CUSTOM_CACERT_PASS}") 31 | fi 32 | 33 | main_command+=("-jar" "${JETTY_HOME}/start.jar") 34 | exec "${main_command[@]}" 35 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base", 5 | ":semanticCommitsDisabled" 6 | ], 7 | "labels": ["dependencies"], 8 | "rebaseWhen": "conflicted", 9 | "ignoreDeps": [ 10 | "org.gretty", 11 | "eclipse-temurin" 12 | ], 13 | "regexManagers": [ 14 | { 15 | "fileMatch": ["src/main/resources/org/jenkinsci/account/taglib/layout.jelly"], 16 | "matchStrings": ["jquery/(?.*?)/jquery.js"], 17 | "depNameTemplate": "org.webjars:jquery", 18 | "datasourceTemplate": "maven" 19 | }, 20 | { 21 | "fileMatch": ["src/main/resources/org/jenkinsci/account/taglib/layout.jelly"], 22 | "matchStrings": ["jquery-ui/(?.*?)/jquery-ui.js"], 23 | "depNameTemplate": "org.webjars:jquery-ui", 24 | "datasourceTemplate": "maven" 25 | }, 26 | { 27 | "fileMatch": ["src/main/resources/org/jenkinsci/account/taglib/layout.jelly"], 28 | "matchStrings": ["fontawesome/(?.*?)/css"], 29 | "depNameTemplate": "org.webjars.bower:fontawesome", 30 | "datasourceTemplate": "maven" 31 | } 32 | ], 33 | "packageRules": [ 34 | { 35 | "groupName": "selenium", 36 | "matchPackagePatterns": [ 37 | "selenium" 38 | ] 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | pipeline { 2 | environment { 3 | JAVA_HOME = '/opt/jdk-17' 4 | } 5 | agent { 6 | // infra.ci build on amd64 to be compliant with selenium 7 | label 'jdk17 || linux-amd64-docker ' 8 | } 9 | options { 10 | disableConcurrentBuilds(abortPrevious: true) 11 | } 12 | 13 | stages { 14 | stage('Build') { 15 | steps { 16 | sh './gradlew build -x test -x integrationTest' 17 | } 18 | } 19 | stage('Test') { 20 | steps { 21 | sh './gradlew check' 22 | } 23 | 24 | post { 25 | always { 26 | junit 'build/test-results/**/TEST-*.xml' 27 | } 28 | unsuccessful { 29 | archiveArtifacts allowEmptyArchive: true, artifacts: 'errorScreenshots/*.jpg' 30 | } 31 | } 32 | } 33 | stage('Docker image') { 34 | steps { 35 | buildDockerAndPublishImage('account-app', [ 36 | rebuildImageOnPeriodicJob: false, 37 | publishToPrivateAzureRegistry: true, 38 | targetplatforms: 'linux/arm64', 39 | disablePublication: !infra.isInfra() 40 | ]) 41 | } 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/it/java/org/jenkinsci/account/ui/admin/AdminSearchPage.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.account.ui.admin; 2 | 3 | import org.openqa.selenium.WebDriver; 4 | import org.openqa.selenium.WebElement; 5 | import org.openqa.selenium.support.FindBy; 6 | import org.openqa.selenium.support.PageFactory; 7 | 8 | // page_url = http://localhost:8080/admin/search 9 | public class AdminSearchPage { 10 | @FindBy(css = "input[value$=\"password\"]") 11 | private WebElement resetPasswordButton; 12 | 13 | @FindBy(name = "email") 14 | private WebElement updateEmailInput; 15 | 16 | @FindBy(css = "input[value=\"Update\"]") 17 | private WebElement updateEmailButton; 18 | 19 | @FindBy(name = "confirm") 20 | private WebElement confirmDeleteUserInput; 21 | 22 | @FindBy(xpath = "//input[@value=\"Delete\"]") 23 | private WebElement deleteUserButton; 24 | 25 | public AdminSearchPage(WebDriver driver) { 26 | PageFactory.initElements(driver, this); 27 | } 28 | 29 | public void resetPassword() { 30 | resetPasswordButton.click(); 31 | } 32 | 33 | public void deleteUser() { 34 | confirmDeleteUserInput.sendKeys("Yes"); 35 | deleteUserButton.click(); 36 | } 37 | 38 | public void updateEmail(String newEmail) { 39 | updateEmailInput.sendKeys(newEmail); 40 | updateEmailButton.click(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/account/UserError.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.account; 2 | 3 | import org.kohsuke.stapler.HttpResponse; 4 | import org.kohsuke.stapler.HttpResponses; 5 | import org.kohsuke.stapler.StaplerRequest; 6 | import org.kohsuke.stapler.StaplerResponse; 7 | 8 | import javax.servlet.ServletException; 9 | import java.io.IOException; 10 | 11 | /** 12 | * Indicates a problem in the user given information. 13 | * Remark: Message are considered as untrusted and therefor are escaped. 14 | * 15 | * @author Kohsuke Kawaguchi 16 | */ 17 | public class UserError extends RuntimeException implements HttpResponse { 18 | private String id; 19 | 20 | public UserError(String message) { 21 | super(message); 22 | } 23 | 24 | /** 25 | * @param message 26 | * error message 27 | * @param id 28 | * ID for matching server logs with Jira issues 29 | */ 30 | public UserError(String message, String id) { 31 | super(message); 32 | this.id = id; 33 | } 34 | 35 | public void generateResponse(StaplerRequest req, StaplerResponse rsp, Object node) throws IOException, ServletException { 36 | rsp.forward(this,"index",req); 37 | } 38 | 39 | /** 40 | * @return ID for matching server logs with Jira issues 41 | */ 42 | public String getId(){ 43 | return id; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/account/AdminUI/signup.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

5 | Confirm creating the following user 6 |

7 |
8 |
9 | 10 | 11 |
12 | 13 |
14 | 15 | 16 |
17 | 18 |
19 | 20 | 21 |
22 | 23 |
24 | 25 | 26 |
27 | 28 | 29 | 30 |
31 |
32 |
33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jenkins Account Management 2 | 3 | ## Testing locally 4 | 5 | With a Docker Engine in "Linux Container" mode and the `compose` plugin installed, 6 | run the command `docker compose up --build -d` which will: 7 | 8 | - Build the application (in a container) 9 | - Start a LDAP server with fixtures 10 | 11 | You can also run only the LDAP stack with `docker compose up -d ldap-data` and then `./gradlew appRun` for local development. 12 | 13 | Both cases will get you a development server running at . 14 | 15 | The default admin username is `kohsuke` and its password is `password` (see the mock-ldap/ directory). 16 | 17 | ### Emails 18 | 19 | Emails are send to a local mail server and not forwarded on, you can see them at . 20 | 21 | ## Packaging 22 | 23 | For deploying to production, this app is packaged as a container. 24 | 25 | To run the container locally: 26 | 27 | ```shell 28 | docker compose up --build app 29 | ``` 30 | 31 | ## SMTP 32 | 33 | The account app support different types of SMTP configuration to send emails: 34 | 35 | - Nothing is configured, the application try to connect on `localhost:25` 36 | - `SMTP_AUTH` is set to false, the accountapp will connect on `$SMTP_SERVER:25` 37 | - `SMTP_AUTH` is set to true, the accountapp will connect on `$SMTP_SERVER:587` with tls authentication 38 | and will use: `$SMTP_USER` with `$SMTP_PASSWORD`. 39 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM eclipse-temurin:17 AS build 2 | 3 | WORKDIR /app 4 | COPY . . 5 | RUN ./gradlew --no-daemon --info war -x test -x integrationTest 6 | 7 | FROM jetty:10.0.26-jre17 AS production 8 | 9 | LABEL \ 10 | description="Deploy Jenkins infra account app" \ 11 | project="https://github.com/jenkins-infra/account-app" \ 12 | maintainer="infra@lists.jenkins-ci.org" 13 | 14 | ENV DD_AGENT_SERVICE_PORT="8126" 15 | ENV CIRCUIT_BREAKER_FILE=/etc/accountapp/circuitBreaker.txt 16 | ENV SMTP_SERVER=localhost 17 | ENV SMTP_SENDER=admin@jenkins-ci.org 18 | ENV APP_URL=http://accounts.jenkins.io/ 19 | 20 | EXPOSE 8080 21 | 22 | USER root 23 | 24 | # /home/jetty/.app is apparently needed by Stapler for some weird reason. O_O 25 | RUN \ 26 | mkdir -p /home/jetty/.app &&\ 27 | mkdir -p /etc/accountapp 28 | 29 | ENV DD_AGENT_VERSION=0.9.0 30 | ADD https://repo1.maven.org/maven2/com/datadoghq/dd-java-agent/$DD_AGENT_VERSION/dd-java-agent-"$DD_AGENT_VERSION".jar /home/jetty/dd-java-agent.jar 31 | 32 | COPY --chown=jetty:root --from=build /app/build/libs/accountapp*.war /var/lib/jetty/webapps/ROOT.war 33 | 34 | COPY entrypoint.sh /entrypoint.sh 35 | 36 | RUN chmod 0755 /entrypoint.sh &&\ 37 | chown -R jetty:root /etc/accountapp &&\ 38 | chown -R jetty:root /var/lib/jetty &&\ 39 | chown -R jetty:root /home/jetty/dd-java-agent.jar 40 | 41 | COPY circuitBreaker.txt /etc/accountapp/circuitBreaker.txt 42 | 43 | USER jetty 44 | 45 | ENTRYPOINT ["bash","/entrypoint.sh"] 46 | -------------------------------------------------------------------------------- /src/it/java/org/jenkinsci/account/ui/login/LoginPage.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.account.ui.login; 2 | 3 | import org.openqa.selenium.WebDriver; 4 | import org.openqa.selenium.WebElement; 5 | import org.openqa.selenium.support.FindBy; 6 | import org.openqa.selenium.support.PageFactory; 7 | 8 | // page_url = http://localhost:8080/login 9 | public class LoginPage { 10 | @FindBy(name = "userid") 11 | private WebElement usernameField; 12 | 13 | @FindBy(name = "password") 14 | private WebElement passwordField; 15 | 16 | @FindBy(xpath = "//button") 17 | private WebElement loginBtn; 18 | 19 | @FindBy(xpath = "//a[@href=\"passwordReset\"]") 20 | private WebElement forgotPasswordLink; 21 | 22 | @FindBy(xpath = "//a[@href=\"signup\"]") 23 | private WebElement signupLink; 24 | 25 | public LoginPage(WebDriver driver) { 26 | PageFactory.initElements(driver, this); 27 | } 28 | 29 | public void login(String username, String password) { 30 | enterUsername(username); 31 | enterPassword(password); 32 | 33 | clickLogin(); 34 | } 35 | 36 | public void clickForgotPassword() { 37 | forgotPasswordLink.click(); 38 | } 39 | 40 | public void clickSignup() { 41 | signupLink.click(); 42 | } 43 | 44 | private void enterUsername(String username) { 45 | usernameField.sendKeys(username); 46 | } 47 | 48 | private void enterPassword(String password) { 49 | passwordField.sendKeys(password); 50 | } 51 | 52 | private void clickLogin() { 53 | loginBtn.click(); 54 | } 55 | } -------------------------------------------------------------------------------- /src/main/webapp/WEB-INF/web.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | BDC_soundEnabled 9 | false 10 | 11 | 12 | 13 | Stapler 14 | org.kohsuke.stapler.Stapler 15 | 16 | default-encodings 17 | text/html=UTF-8 18 | 19 | 20 | 21 | 22 | Stapler 23 | /* 24 | 25 | 26 | 27 | 28 | WebjarsServlet 29 | org.webjars.servlet.WebjarsServlet 30 | 31 | 32 | WebjarsServlet 33 | /webjars/* 34 | 35 | 36 | 37 | BotDetect Captcha 38 | com.captcha.botdetect.web.servlet.CaptchaServlet 39 | 40 | 41 | BotDetect Captcha 42 | /botdetectcaptcha 43 | 44 | 45 | 46 | org.jenkinsci.account.WebAppMain 47 | 48 | 49 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/account/PasswordUtil.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.account; 2 | 3 | import org.apache.commons.codec.binary.Base64; 4 | 5 | import java.security.MessageDigest; 6 | import java.security.NoSuchAlgorithmException; 7 | import java.security.SecureRandom; 8 | 9 | /** 10 | * @author Kohsuke Kawaguchi 11 | */ 12 | public class PasswordUtil { 13 | private static final SecureRandom random = new SecureRandom(); 14 | 15 | /** 16 | * Java version of 'slappasswd'. 17 | * See http://www.securitydocs.com/library/3439 18 | */ 19 | public static synchronized String hashPassword(String password) { 20 | try { 21 | byte[] salt = new byte[4]; 22 | random.nextBytes(salt); 23 | MessageDigest sha = MessageDigest.getInstance("SHA-1"); 24 | sha.update(password.getBytes()); 25 | sha.update(salt); 26 | byte[] hash = sha.digest(); 27 | 28 | return "{SSHA}"+ Base64.encodeBase64String(concat(hash,salt)); 29 | } catch (NoSuchAlgorithmException e) { 30 | throw new AssertionError(e); 31 | } 32 | } 33 | 34 | private static byte[] concat(byte[] a, byte[] b) { 35 | byte[] r = new byte[a.length+b.length]; 36 | System.arraycopy(a,0,r,0,a.length); 37 | System.arraycopy(b,0,r,a.length,b.length); 38 | return r; 39 | } 40 | 41 | public static String generateRandomPassword() { 42 | String seed = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; 43 | String r = ""; 44 | for (int i=0; i<14; i++) 45 | r+=seed.charAt(random.nextInt(seed.length())); 46 | return r; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/account/AdminUI/index.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 |

Admin

4 |

Manage users

5 |

6 | Type the username or email address to find the user. 7 |

8 |
9 |
10 | 11 | 12 |
13 | 14 | 17 |
18 | 19 |

Set circuit breaker

20 |

21 | Temporarily disable sign-up to fight spam until a certain time. 22 | All times are in UTC. 23 |

24 | 25 |

26 | Circuit breaker is currently on until ${it.circuitBreaker.date}. 27 | To disable it, set it to the time in the past, such as 2000/01/01 00:00. 28 |

29 |
30 |
31 |
32 | 33 | 34 |
35 | 38 |
39 |
40 |
41 | -------------------------------------------------------------------------------- /mock-ldap/data.ldif: -------------------------------------------------------------------------------- 1 | # 2 | # Dummy user database for testing 3 | # 4 | # User 'kohsuke' is admin, user 'alice' is a regular user. 5 | # Both has the password 'password' 6 | # 7 | 8 | dn: dc=jenkins-ci,dc=org 9 | objectClass: top 10 | objectClass: dcObject 11 | objectClass: organization 12 | o: Jenkins users 13 | dc: Jenkins-Ci 14 | description: Jenkins users 15 | 16 | #dn: cn=admin,dc=jenkins-ci,dc=org 17 | #objectClass: simpleSecurityObject 18 | #objectClass: organizationalRole 19 | #cn: admin 20 | #description: LDAP administrator 21 | #userPassword: {SSHA}yI6cZwQadOA1e+/f+T+H3eCQQhRzYWx0 22 | 23 | dn: ou=people,dc=jenkins-ci,dc=org 24 | objectClass: organizationalUnit 25 | ou: people 26 | 27 | dn: ou=groups,dc=jenkins-ci,dc=org 28 | objectClass: organizationalUnit 29 | ou: groups 30 | 31 | dn: cn=admins,ou=groups,dc=jenkins-ci,dc=org 32 | objectClass: groupOfNames 33 | cn: admins 34 | description: people with infrastructure admin access 35 | member: cn=kohsuke,ou=people,dc=jenkins-ci,dc=org 36 | 37 | dn: cn=all,ou=groups,dc=jenkins-ci,dc=org 38 | objectClass: groupOfNames 39 | cn: all 40 | member: cn=kohsuke,ou=people,dc=jenkins-ci,dc=org 41 | member: cn=kohsuke2,ou=people,dc=jenkins-ci,dc=org 42 | 43 | dn: cn=kohsuke,ou=people,dc=jenkins-ci,dc=org 44 | objectClass: inetOrgPerson 45 | cn: kohsuke 46 | mail: kk@kohsuke.org 47 | givenName: Kohsuke 48 | employeeNumber: kohsuke 49 | preferredLanguage: yyy 50 | sn: Kawaguchi 51 | userPassword: {SSHA}yI6cZwQadOA1e+/f+T+H3eCQQhRzYWx0 52 | 53 | dn: cn=alice,ou=people,dc=jenkins-ci,dc=org 54 | objectClass: inetOrgPerson 55 | cn: alice 56 | mail: bob@jenkins-ci.org 57 | givenName: Alice 58 | employeeNumber: alice 59 | sn: Ashley 60 | userPassword: {SSHA}yI6cZwQadOA1e+/f+T+H3eCQQhRzYWx0 61 | -------------------------------------------------------------------------------- /src/it/resources/org/jenkinsci/account/ui/test-data.ldif: -------------------------------------------------------------------------------- 1 | # Dummy user database for testing 2 | # 3 | # User 'kohsuke' is admin, user 'alice' is a regular user. 4 | # Both has the password 'password' 5 | # 6 | dn: dc=jenkins-ci,dc=org 7 | objectClass: top 8 | objectClass: dcObject 9 | objectClass: organization 10 | o: Jenkins users 11 | dc: Jenkins-Ci 12 | description: Jenkins users 13 | 14 | dn: cn=admin,dc=jenkins-ci,dc=org 15 | objectClass: simpleSecurityObject 16 | objectClass: organizationalRole 17 | cn: admin 18 | description: LDAP administrator 19 | userPassword: {SSHA}9wvET88gyrc9QwvQeGwPmvHdblVzYWx0 20 | 21 | dn: ou=people,dc=jenkins-ci,dc=org 22 | objectClass: organizationalUnit 23 | ou: people 24 | 25 | dn: ou=groups,dc=jenkins-ci,dc=org 26 | objectClass: organizationalUnit 27 | ou: groups 28 | 29 | dn: cn=admins,ou=groups,dc=jenkins-ci,dc=org 30 | objectClass: groupOfNames 31 | cn: admins 32 | description: people with infrastructure admin access 33 | member: cn=kohsuke,ou=people,dc=jenkins-ci,dc=org 34 | 35 | dn: cn=all,ou=groups,dc=jenkins-ci,dc=org 36 | objectClass: groupOfNames 37 | cn: all 38 | member: cn=kohsuke,ou=people,dc=jenkins-ci,dc=org 39 | member: cn=kohsuke2,ou=people,dc=jenkins-ci,dc=org 40 | 41 | dn: cn=kohsuke,ou=people,dc=jenkins-ci,dc=org 42 | objectClass: inetOrgPerson 43 | cn: kohsuke 44 | mail: kk@kohsuke.org 45 | givenName: Kohsuke 46 | employeeNumber: kohsuke 47 | preferredLanguage: yyy 48 | sn: Kawaguchi 49 | userPassword: {SSHA}yI6cZwQadOA1e+/f+T+H3eCQQhRzYWx0 50 | 51 | dn: cn=alice,ou=people,dc=jenkins-ci,dc=org 52 | objectClass: inetOrgPerson 53 | cn: alice 54 | mail: bob@jenkins-ci.org 55 | givenName: Alice 56 | employeeNumber: alice 57 | sn: Ashley 58 | userPassword: {SSHA}yI6cZwQadOA1e+/f+T+H3eCQQhRzYWx0 59 | -------------------------------------------------------------------------------- /src/it/java/org/jenkinsci/account/ui/admin/DeleteUserTest.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.account.ui.admin; 2 | 3 | import org.jenkinsci.account.ui.BaseTest; 4 | import org.jenkinsci.account.ui.login.LoginPage; 5 | import org.jenkinsci.account.ui.myaccount.MyAccountPage; 6 | import org.jenkinsci.account.ui.resetpassword.UserLookupType; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import static org.assertj.core.api.Assertions.assertThat; 10 | 11 | public class DeleteUserTest extends BaseTest { 12 | 13 | @Test 14 | public void deleteUserByUsername() { 15 | deleteUser("alice", "bob@jenkins-ci.org", UserLookupType.USERNAME); 16 | } 17 | 18 | @Test 19 | public void deleteUserByEmail() { 20 | deleteUser("alice", "bob@jenkins-ci.org", UserLookupType.EMAIL); 21 | } 22 | 23 | public void deleteUser(String username, String email, UserLookupType userLookupType) { 24 | openHomePage(); 25 | LoginPage loginPage = new LoginPage(driver); 26 | loginPage.login("kohsuke", "password"); 27 | 28 | MyAccountPage myAccountPage = new MyAccountPage(driver); 29 | myAccountPage.clickAdminLink(); 30 | 31 | AdminPage adminPage = new AdminPage(driver); 32 | if (userLookupType == UserLookupType.USERNAME) { 33 | adminPage.search(username); 34 | } else { 35 | adminPage.search(email); 36 | } 37 | 38 | AdminSearchPage adminSearchPage = new AdminSearchPage(driver); 39 | adminSearchPage.deleteUser(); 40 | adminPage.verifyOnPage(); 41 | 42 | newSession(); 43 | 44 | loginPage = new LoginPage(driver); 45 | loginPage.login("alice", "password"); 46 | assertThat(driver.getTitle()).isEqualTo("Error | Jenkins"); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/account/WebAppMain.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.account; 2 | 3 | import com.typesafe.config.Config; 4 | import com.typesafe.config.ConfigFactory; 5 | import org.jenkinsci.account.config.LdapConfig; 6 | import org.jenkinsci.account.config.MailConfig; 7 | import org.kohsuke.stapler.framework.AbstractWebAppMain; 8 | import org.kohsuke.stapler.jelly.DefaultScriptInvoker; 9 | 10 | /** 11 | * Bootstrap code. 12 | * 13 | * @author Kohsuke Kawaguchi 14 | */ 15 | public class WebAppMain extends AbstractWebAppMain { 16 | public WebAppMain() { 17 | super(Application.class); 18 | DefaultScriptInvoker.COMPRESS_BY_DEFAULT = false; // blind shot 19 | } 20 | 21 | @Override 22 | protected String getApplicationName() { 23 | return "APP"; 24 | } 25 | 26 | @Override 27 | public Application createApplication() throws Exception { 28 | Config conf = ConfigFactory.load(); 29 | 30 | LdapConfig ldapConfig = new LdapConfig( 31 | conf.getString("ldap.server"), 32 | conf.getString("ldap.managerDN"), 33 | conf.getString("ldap.managerPassword"), 34 | conf.getString("ldap.newUserBaseDN") 35 | ); 36 | 37 | MailConfig mailConfig = new MailConfig( 38 | conf.getString("mail.server"), 39 | conf.getString("mail.sender"), 40 | conf.getInt("mail.port"), 41 | conf.hasPath("mail.user") ? conf.getString("mail.user") : null, 42 | conf.hasPath("mail.password") ? conf.getString("mail.password") : null, 43 | conf.getBoolean("mail.useAuth") 44 | ); 45 | 46 | Parameters parameters = new Parameters( 47 | conf.getString("url"), 48 | ldapConfig, 49 | mailConfig, 50 | conf.hasPath("circuitBreakerFile") ? conf.getString("circuitBreakerFile") : null 51 | ); 52 | 53 | return new Application(parameters); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/account/AdminUI/search.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 |

Search results

10 |
11 | To delete, type "YES" to the text field left of "Delete" then click the Delete button. 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 33 | 40 | 41 | 48 | 49 | 50 |
User IDEmail addressReset passwordUpdate emailDelete user
${u.id}${u.mail} 28 |
29 | 30 | 31 |
32 |
34 |
35 | 36 | 37 | 38 |
39 |
42 |
43 | 44 | 45 | 46 |
47 |
51 |
52 |
53 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/account/Application/signup.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Register

5 | 6 |
All fields are required
7 | 8 |
9 |
10 | 11 |
12 | Only letters, numbers, and '_' is allowed. 13 |
14 | 15 |
16 | 17 |
18 | 19 | 20 |
21 | 22 |
23 | 24 | 25 |
26 | 27 |
28 | 29 | 30 |
31 | 32 |
33 | 34 | 35 |
36 | 37 | 38 | 39 | 42 | 43 |
44 | 45 | 46 | 47 |
48 | 49 | 50 |
51 |
52 |
53 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/account/openid/JenkinsOpenIDSession.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.account.openid; 2 | 3 | import org.jenkinsci.account.Myself; 4 | import org.kohsuke.stapler.HttpResponse; 5 | import org.kohsuke.stapler.HttpResponses; 6 | import org.kohsuke.stapler.interceptor.RequirePOST; 7 | import org.kohsuke.stapler.openid.server.*; 8 | import org.kohsuke.stapler.openid.server.Session; 9 | 10 | import java.net.MalformedURLException; 11 | import java.net.URL; 12 | import java.util.HashSet; 13 | import java.util.Set; 14 | 15 | /** 16 | * @author Kohsuke Kawaguchi 17 | */ 18 | public class JenkinsOpenIDSession extends Session { 19 | private final Set approvedRealms = new HashSet(); 20 | private final JenkinsOpenIDServer server; 21 | 22 | 23 | public JenkinsOpenIDSession(JenkinsOpenIDServer server) { 24 | super(server); 25 | this.server = server; 26 | } 27 | 28 | @Override 29 | protected HttpResponse authenticateUser(OpenIDIdentity id) { 30 | Myself myself = server.app.getMyself(); 31 | 32 | id .withFirstName(myself.firstName) 33 | .withLastName(myself.lastName) 34 | .withFullName(myself.firstName + ' ' + myself.lastName) 35 | .withNick(myself.userId); 36 | // not passing email 37 | 38 | if (!isApproved()) { 39 | // let's confirm the user, which will take them to doVerify 40 | return HttpResponses.forwardToView(this, "confirm"); 41 | } 42 | 43 | return null; 44 | } 45 | 46 | /** 47 | * Returns true if the login for the specified realm/return_to location is ACKed by the user. 48 | */ 49 | private boolean isApproved() { 50 | if (approvedRealms.contains(getRealm())) return true; // explicitly approved 51 | 52 | try { 53 | if (new URL(getReturnTo()).getHost().endsWith(".jenkins-ci.org")) 54 | return true; // apps in our own domains are trusted 55 | } catch (MalformedURLException e) { 56 | // fall through 57 | } 58 | 59 | return false; 60 | } 61 | 62 | @RequirePOST 63 | public HttpResponse doVerify() { 64 | approvedRealms.add(getRealm()); 65 | return handleRequest(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/account/CircuitBreaker.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.account; 2 | 3 | import org.apache.commons.io.FileUtils; 4 | import org.kohsuke.stapler.HttpResponse; 5 | import org.kohsuke.stapler.HttpResponses; 6 | import org.kohsuke.stapler.QueryParameter; 7 | import org.kohsuke.stapler.interceptor.RequirePOST; 8 | 9 | import java.io.File; 10 | import java.io.IOException; 11 | import java.text.ParseException; 12 | import java.text.SimpleDateFormat; 13 | import java.util.Date; 14 | import java.util.logging.Logger; 15 | 16 | /** 17 | * Temporarily shut-off valve to disable sign-up. 18 | * 19 | * @author Kohsuke Kawaguchi 20 | */ 21 | public class CircuitBreaker { 22 | private final File file; 23 | 24 | public CircuitBreaker(File file) { 25 | this.file = file; 26 | } 27 | 28 | public CircuitBreaker(Parameters params) { 29 | String f = params.circuitBreakerFile(); 30 | if (f!=null) 31 | this.file = new File(f); 32 | else 33 | this.file = new File("/no-such-file"); 34 | } 35 | 36 | public boolean isOn() { 37 | return System.currentTimeMillis() < file.lastModified(); 38 | } 39 | 40 | /** 41 | * Throws an exception if the circuit breaker is on. 42 | */ 43 | public boolean check() throws IOException { 44 | if (isOn()) { 45 | LOGGER.info("Rejecting sign up due to circuit breaker"); 46 | return true; 47 | } 48 | return false; 49 | } 50 | 51 | @RequirePOST 52 | public HttpResponse doSet(@QueryParameter String time) throws ParseException { 53 | Date t = makeFormatter().parse(time.trim()); 54 | file.setLastModified(t.getTime()); 55 | return HttpResponses.plainText("Successfully set"); 56 | } 57 | 58 | /** 59 | * Current effective date 60 | */ 61 | public String getDate() { 62 | long dt = file.lastModified(); 63 | if (dt<=0) 64 | return "(none)"; 65 | else 66 | return makeFormatter().format(new Date(dt)); 67 | } 68 | 69 | private SimpleDateFormat makeFormatter() { 70 | return new SimpleDateFormat("yyyy/MM/dd HH:mm"); 71 | } 72 | 73 | private static final Logger LOGGER = Logger.getLogger(CircuitBreaker.class.getName()); 74 | } 75 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/account/Myself/index.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Your profile

5 | 6 |
7 |
8 | 9 | 10 |
11 | 12 |
13 | 14 | 15 |
16 | 17 |
18 | 19 | 20 |
21 | 22 |
23 | 24 | 25 |
26 | 27 |
28 | 29 | 30 |
31 | 32 |
33 | 34 | 35 |
36 | 37 |

Change Password

38 |

39 | To update your password, please type your current password as well as new one for security. 40 | Leave this empty to keep the current password. 41 |

42 | 43 |
44 | 45 | 46 |
47 | 48 |
49 | 50 | 51 |
52 | 53 |
54 | 55 | 56 |
57 | 58 | 59 |
60 |
61 |
62 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | certs: 3 | image: jenkinsciinfra/ldap:latest 4 | volumes: 5 | - certs:/certs:rw 6 | entrypoint: 7 | - "/bin/bash" 8 | - -eucx 9 | - | 10 | if test -f /certs/rootCA.pem; then exit 0;fi 11 | apt-get update -q 12 | apt-get install --yes --no-install-recommends curl 13 | curl --silent --show-error --location "https://dl.filippo.io/mkcert/latest?for=linux/$(dpkg --print-architecture)" \ 14 | --output /usr/local/bin/mkcert 15 | chmod +x /usr/local/bin/mkcert 16 | mkcert -key-file /certs/privkey.key -cert-file /certs/cert.pem ldap ldap.localhost localhost 127.0.0.1 ::1 17 | cp "$(mkcert -CAROOT)"/rootCA.pem /certs/rootCA.pem 18 | chown 101 /certs/* 19 | chmod 0664 /certs/* 20 | 21 | ldap: 22 | image: jenkinsciinfra/ldap:latest 23 | depends_on: 24 | certs: 25 | condition: service_completed_successfully 26 | environment: 27 | OPENLDAP_SSL_CA_ROOTDIR: /etc/ldap/ssl 28 | OPENLDAP_SSL_CA: rootCA.pem 29 | volumes: 30 | - certs:/etc/ldap/ssl/:ro 31 | ports: 32 | - "1636:636" 33 | - "1389:389" 34 | healthcheck: 35 | test: ["CMD", "cat", "/run/slapd/slapd.pid"] 36 | interval: 5s 37 | timeout: 5s 38 | retries: 3 39 | start_period: 5s 40 | 41 | ldap-data: 42 | image: jenkinsciinfra/ldap:latest 43 | depends_on: 44 | ldap: 45 | condition: service_healthy 46 | # exit code 68 in LDAP means entries already exists: we consider this case a success 47 | entrypoint: bash -c 'ldapmodify -H ldap://ldap -x -w "$${OPENLDAP_ADMIN_PASSWORD}" -D "$${OPENLDAP_ADMIN_DN}" -a -c -f /var/backups/backup.latest.ldif || [ $? -eq 68 ] || exit 1' 48 | volumes: 49 | - ./mock-ldap/data.ldif:/var/backups/backup.latest.ldif:ro 50 | 51 | mail: 52 | image: rnwood/smtp4dev 53 | ports: 54 | - "3000:80" 55 | - "2525:25" 56 | - "1143:143" 57 | 58 | app: 59 | build: . 60 | environment: 61 | - LDAP_URL=ldaps://ldap:636 62 | - CUSTOM_CERT_FILE=/certs/rootCA.pem 63 | depends_on: 64 | ldap: 65 | condition: service_healthy 66 | ldap-data: 67 | condition: service_completed_successfully 68 | mail: 69 | condition: service_started 70 | volumes: 71 | - certs:/certs:ro 72 | ports: 73 | - '8080:8080' 74 | 75 | volumes: 76 | certs: 77 | -------------------------------------------------------------------------------- /src/it/java/org/jenkinsci/account/ui/resetpassword/ResetPasswordTest.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.account.ui.resetpassword; 2 | 3 | import jakarta.mail.MessagingException; 4 | import java.io.IOException; 5 | import java.util.Date; 6 | import java.util.regex.Matcher; 7 | import org.jenkinsci.account.ui.BaseTest; 8 | import org.jenkinsci.account.ui.email.Emails; 9 | import org.jenkinsci.account.ui.login.LoginPage; 10 | import org.junit.jupiter.api.Test; 11 | 12 | import static org.assertj.core.api.Assertions.assertThat; 13 | 14 | class ResetPasswordTest extends BaseTest { 15 | 16 | @Test 17 | void resetPasswordWithUsername() throws MessagingException, IOException { 18 | resetPassword("alice", "bob@jenkins-ci.org", UserLookupType.USERNAME); 19 | } 20 | 21 | @Test 22 | void resetPasswordWithEmail() throws MessagingException, IOException { 23 | resetPassword("alice", "bob@jenkins-ci.org", UserLookupType.EMAIL); 24 | } 25 | 26 | private void resetPassword(String username, String email, UserLookupType userLookupType) throws MessagingException, IOException { 27 | openHomePage(); 28 | 29 | LoginPage loginPage = new LoginPage(driver); 30 | loginPage.clickForgotPassword(); 31 | 32 | Date timestampBeforeReset = new Date(); 33 | 34 | ResetPasswordPage resetPasswordPage = new ResetPasswordPage(driver); 35 | if (userLookupType == UserLookupType.USERNAME) { 36 | resetPasswordPage.resetPassword(username); 37 | } else { 38 | resetPasswordPage.resetPassword(email); 39 | } 40 | 41 | String text = resetPasswordPage.resultText(); 42 | assertThat(text).contains("If your user account or email address exists"); 43 | 44 | String emailContent = READ_INBOUND_EMAIL_SERVICE 45 | .retrieveEmail( 46 | email, 47 | Emails.RESET_PASSWORD_SUBJECT, 48 | timestampBeforeReset 49 | ); 50 | 51 | assertThat(emailContent).isNotEmpty(); 52 | 53 | Matcher matcher = Emails.PASSWORD_EXTRACTOR.matcher(emailContent); 54 | boolean matches = matcher.find(); 55 | assertThat(matches).isTrue(); 56 | 57 | String password = matcher.group(1); 58 | 59 | openHomePage(); 60 | loginPage.login(username, password); 61 | 62 | String pageTitle = driver.getTitle(); 63 | assertThat(pageTitle).contains("Account"); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/java/BulkImport.java: -------------------------------------------------------------------------------- 1 | import org.jenkinsci.account.Application; 2 | import org.jenkinsci.account.WebAppMain; 3 | 4 | import javax.naming.NameAlreadyBoundException; 5 | import javax.naming.NamingEnumeration; 6 | import javax.naming.directory.SearchControls; 7 | import javax.naming.directory.SearchResult; 8 | import javax.naming.ldap.LdapContext; 9 | import java.io.File; 10 | import java.util.HashSet; 11 | import java.util.Set; 12 | 13 | /** 14 | * Import user file into LDAP. 15 | * 16 | * This was a one-off tool used during the initial population of LDAP. 17 | * Left only for historical archival purpose. 18 | * 19 | * @author Kohsuke Kawaguchi 20 | */ 21 | public class BulkImport { 22 | public static void main(String[] args) throws Exception { 23 | Application app = new WebAppMain().createApplication(); 24 | 25 | Set names = new HashSet(); 26 | SearchControls cons = new SearchControls(); 27 | cons.setReturningAttributes(new String[]{"cn"}); 28 | NamingEnumeration e = app.connect().search("ou=people,dc=jenkins-ci,dc=org", "(objectClass=inetOrgPerson)", cons); 29 | while (e.hasMore()) { 30 | SearchResult r = e.nextElement(); 31 | String cn = (String)r.getAttributes().get("cn").get(); 32 | names.add(cn); 33 | } 34 | 35 | {// clean up bogus entries 36 | LdapContext ldap = app.connect(); 37 | for (String name : names) { 38 | if (!name.toLowerCase().equals(name) || name.contains("@")) { 39 | // delete this 40 | System.out.println("Deleting "+name); 41 | ldap.destroySubcontext("cn=" + name + ",ou=people,dc=jenkins-ci,dc=org"); 42 | } 43 | } 44 | } 45 | 46 | File dir = new File(args[0]); 47 | System.out.println("Listing up "+dir); 48 | for (File f : dir.listFiles()) { 49 | if (f.exists() && !f.isDirectory()) { 50 | String name =f.getName().toLowerCase(); 51 | System.out.println(name); 52 | if (name.contains("@")) continue; // invalid 53 | 54 | if (names.contains(name)) continue; // already imported 55 | 56 | try { 57 | app.createRecord(name,f.getName(),"-",f.getName()+"@java.net"); 58 | } catch (NameAlreadyBoundException x) { 59 | // already registered. move on 60 | } 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/it/java/org/jenkinsci/account/ui/email/ReadInboundEmailService.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.account.ui.email; 2 | 3 | import jakarta.mail.Folder; 4 | import jakarta.mail.Message; 5 | import jakarta.mail.MessagingException; 6 | import jakarta.mail.Session; 7 | import jakarta.mail.Store; 8 | import jakarta.mail.internet.InternetAddress; 9 | import java.io.IOException; 10 | import java.util.Arrays; 11 | import java.util.Date; 12 | import java.util.Properties; 13 | import com.sun.mail.imap.IMAPFolder; 14 | 15 | public class ReadInboundEmailService { 16 | private final String host; 17 | private final int port; 18 | 19 | public ReadInboundEmailService(String host, int port) { 20 | this.host = host; 21 | this.port = port; 22 | } 23 | 24 | public String retrieveEmail(String toAddressToSearchFor, String subjectToSearchFor, Date beginningOfTimeWindow) throws MessagingException, IOException { 25 | Session session = this.getImapSession(); 26 | Store store = session.getStore("imap"); 27 | store.connect(host, port, toAddressToSearchFor, toAddressToSearchFor); 28 | IMAPFolder inbox = (IMAPFolder) store.getFolder("INBOX"); 29 | inbox.open(Folder.READ_WRITE); 30 | Message[] messages = inbox.getMessages(); 31 | 32 | return Arrays.stream(messages) 33 | .filter(message -> { 34 | try { 35 | return ((InternetAddress)message.getRecipients(Message.RecipientType.TO)[0]) 36 | .getAddress() 37 | .equals(toAddressToSearchFor) 38 | && 39 | message.getSubject().equals(subjectToSearchFor) 40 | && 41 | beginningOfTimeWindow.getTime() - 1000 < message.getReceivedDate().getTime(); 42 | } catch (MessagingException e) { 43 | throw new RuntimeException(e); 44 | } 45 | }) 46 | .map(message -> { 47 | try { 48 | return String.valueOf(message.getContent()); 49 | } catch (IOException | MessagingException e) { 50 | throw new RuntimeException(e); 51 | } 52 | }).findFirst() 53 | .orElse(null); 54 | } 55 | 56 | private Session getImapSession() { 57 | Properties props = new Properties(); 58 | props.setProperty("mail.store.protocol", "imap"); 59 | props.setProperty("mail.imap.host", host); 60 | props.setProperty("mail.imap.port", String.valueOf(port)); 61 | return Session.getDefaultInstance(props, null); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/it/java/org/jenkinsci/account/ui/myaccount/MyProfilePage.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.account.ui.myaccount; 2 | 3 | import org.openqa.selenium.WebDriver; 4 | import org.openqa.selenium.WebElement; 5 | import org.openqa.selenium.support.FindBy; 6 | import org.openqa.selenium.support.PageFactory; 7 | import java.util.List; 8 | 9 | // page_url = http://localhost:8080/myself/ 10 | public class MyProfilePage { 11 | @FindBy(name = "firstName") 12 | private WebElement firstNameInput; 13 | 14 | @FindBy(name = "lastName") 15 | private WebElement lastNameInput; 16 | 17 | @FindBy(name = "email") 18 | private WebElement emailInput; 19 | 20 | @FindBy(name = "githubId") 21 | private WebElement githubInput; 22 | 23 | @FindBy(xpath = "//textarea") 24 | private WebElement sshKeysInput; 25 | 26 | @FindBy(name = "password") 27 | private WebElement currentPasswordInput; 28 | 29 | @FindBy(css = "input[name=\"newPassword1\"]") 30 | private WebElement newPasswordInput; 31 | 32 | @FindBy(css = "input[name=\"newPassword2\"]") 33 | private WebElement newPasswordConfirmInput; 34 | 35 | @FindBy(xpath = "//button[@type=\"submit\"]") 36 | private WebElement updateButton; 37 | 38 | public MyProfilePage(WebDriver driver) { 39 | PageFactory.initElements(driver, this); 40 | } 41 | 42 | public void updateFirstName(String text) { 43 | firstNameInput.clear(); 44 | firstNameInput.sendKeys(text); 45 | } 46 | 47 | public void updateLastName(String text) { 48 | lastNameInput.clear(); 49 | lastNameInput.sendKeys(text); 50 | } 51 | 52 | public void updateEmail(String text) { 53 | emailInput.clear(); 54 | emailInput.sendKeys(text); 55 | } 56 | 57 | public void updateGitHub(String text) { 58 | githubInput.clear(); 59 | githubInput.sendKeys(text); 60 | } 61 | 62 | public void updateSSHKeys(String text) { 63 | sshKeysInput.clear(); 64 | sshKeysInput.sendKeys(text); 65 | } 66 | 67 | public void changePassword(String oldPassword, String newPassword) { 68 | updateOldPassword(oldPassword); 69 | updateNewPassword(newPassword); 70 | updateConfirmNewPassword(newPassword); 71 | 72 | updateButton.click(); 73 | } 74 | 75 | public void updateConfirmNewPassword(String newPassword) { 76 | newPasswordConfirmInput.sendKeys(newPassword); 77 | } 78 | 79 | public void updateNewPassword(String newPassword) { 80 | newPasswordInput.sendKeys(newPassword); 81 | } 82 | 83 | public void updateOldPassword(String oldPassword) { 84 | currentPasswordInput.sendKeys(oldPassword); 85 | } 86 | 87 | public void update() { 88 | updateButton.click(); 89 | } 90 | } -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/account/Parameters.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.account; 2 | 3 | import org.jenkinsci.account.config.LdapConfig; 4 | import org.jenkinsci.account.config.MailConfig; 5 | 6 | /** 7 | * Configuration of the application that needs to be set outside the application. 8 | * 9 | * @author Kohsuke Kawaguchi 10 | */ 11 | public class Parameters { 12 | 13 | private final String url; 14 | private final LdapConfig ldapConfig; 15 | private final MailConfig mailConfig; 16 | private final String circuitBreakerFile; 17 | 18 | public Parameters(String url, LdapConfig ldapConfig, MailConfig mailConfig, String circuitBreakerFile) { 19 | this.url = url; 20 | this.ldapConfig = ldapConfig; 21 | this.mailConfig = mailConfig; 22 | this.circuitBreakerFile = circuitBreakerFile; 23 | } 24 | 25 | /** 26 | * string like "ou=people,dc=acme,dc=com" that decides where new users are created. 27 | */ 28 | public String newUserBaseDN() { 29 | return ldapConfig.getNewUserBaseDn(); 30 | } 31 | 32 | /** 33 | * Coordinates to access LDAP. 34 | */ 35 | public String managerDN() { 36 | return ldapConfig.getManagerDn(); 37 | } 38 | public String managerPassword() { 39 | return ldapConfig.getManagerPassword(); 40 | } 41 | public String server() { 42 | return ldapConfig.getServer(); 43 | } 44 | 45 | /** 46 | * smtpServer: The SMTP server to connect to. 47 | * smtpSender: The sender email address used to send emails 48 | * smtpUser: Default user name for SMTP. 49 | * smtpAuth: If true, attempt to authenticate the user using the AUTH command. 50 | * smtpPassword: SMTP password for SMTP server. 51 | */ 52 | public boolean smtpAuth() { 53 | return mailConfig.isSmtpAuth(); 54 | } 55 | public String smtpServer() { 56 | return mailConfig.getSmtpServer(); 57 | } 58 | public String smtpSender() { 59 | return mailConfig.getSmtpSender(); 60 | } 61 | public String smtpUser() { 62 | return mailConfig.getSmtpUser(); 63 | } 64 | public String smtpPassword() { 65 | return mailConfig.getSmtpPassword(); 66 | } 67 | 68 | public int smtpPort() { 69 | return mailConfig.getSmtpPort(); 70 | } 71 | 72 | /** 73 | * HTTP URL that this application is running. Something like 'https://accounts.jenkins.io/'. Must end with '/'. 74 | */ 75 | public String url() { 76 | return url; 77 | } 78 | 79 | /** 80 | * File that activates a circuit breaker, a temporary shutdown of a sign-up service. 81 | */ 82 | 83 | public String circuitBreakerFile() { 84 | return circuitBreakerFile; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/it/java/org/jenkinsci/account/ui/admin/ResetPasswordAdminTest.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.account.ui.admin; 2 | 3 | import jakarta.mail.MessagingException; 4 | import java.io.IOException; 5 | import java.util.Date; 6 | import java.util.regex.Matcher; 7 | import org.jenkinsci.account.ui.BaseTest; 8 | import org.jenkinsci.account.ui.email.Emails; 9 | import org.jenkinsci.account.ui.login.LoginPage; 10 | import org.jenkinsci.account.ui.myaccount.MyAccountPage; 11 | import org.jenkinsci.account.ui.resetpassword.UserLookupType; 12 | import org.junit.jupiter.api.Test; 13 | 14 | import static org.assertj.core.api.Assertions.assertThat; 15 | 16 | public class ResetPasswordAdminTest extends BaseTest { 17 | 18 | @Test 19 | void resetPasswordWithUsername() throws MessagingException, IOException { 20 | resetPassword("alice", "bob@jenkins-ci.org", UserLookupType.USERNAME); 21 | } 22 | 23 | @Test 24 | void resetPasswordWithEmail() throws MessagingException, IOException { 25 | resetPassword("alice", "bob@jenkins-ci.org", UserLookupType.EMAIL); 26 | } 27 | 28 | private void resetPassword(String username, String email, UserLookupType userLookupType) throws MessagingException, IOException { 29 | openHomePage(); 30 | LoginPage loginPage = new LoginPage(driver); 31 | loginPage.login("kohsuke", "password"); 32 | 33 | MyAccountPage myAccountPage = new MyAccountPage(driver); 34 | myAccountPage.clickAdminLink(); 35 | 36 | AdminPage adminPage = new AdminPage(driver); 37 | if (userLookupType == UserLookupType.USERNAME) { 38 | adminPage.search(username); 39 | } else { 40 | adminPage.search(email); 41 | } 42 | 43 | Date timestampBeforeReset = new Date(); 44 | 45 | AdminSearchPage adminSearchPage = new AdminSearchPage(driver); 46 | adminSearchPage.resetPassword(); 47 | 48 | AdminResetPasswordResultPage resetPasswordResultPage = new AdminResetPasswordResultPage(driver); 49 | String newPassword = resetPasswordResultPage.getNewPassword(); 50 | 51 | String emailContent = READ_INBOUND_EMAIL_SERVICE 52 | .retrieveEmail( 53 | email, 54 | Emails.RESET_PASSWORD_SUBJECT, 55 | timestampBeforeReset 56 | ); 57 | 58 | Matcher matcher = Emails.PASSWORD_EXTRACTOR.matcher(emailContent); 59 | boolean matches = matcher.find(); 60 | assertThat(matches).isTrue(); 61 | 62 | String passwordFromEmail = matcher.group(1); 63 | 64 | assertThat(newPassword).isEqualTo(passwordFromEmail); 65 | 66 | newSession(); 67 | 68 | new LoginPage(driver).login(username, newPassword); 69 | String pageTitle = driver.getTitle(); 70 | assertThat(pageTitle).contains("Account"); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/account/batch/UpdateSeniorGroup.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.account.batch; 2 | 3 | import org.jenkinsci.account.Application; 4 | import org.jenkinsci.account.WebAppMain; 5 | 6 | import javax.naming.NamingEnumeration; 7 | import javax.naming.directory.Attribute; 8 | import javax.naming.directory.AttributeInUseException; 9 | import javax.naming.directory.BasicAttributes; 10 | import javax.naming.directory.SearchControls; 11 | import javax.naming.directory.SearchResult; 12 | import javax.naming.ldap.LdapContext; 13 | import java.text.ParseException; 14 | import java.text.SimpleDateFormat; 15 | import java.util.concurrent.TimeUnit; 16 | 17 | import static javax.naming.directory.DirContext.*; 18 | import static org.jenkinsci.account.LdapAbuse.*; 19 | 20 | /** 21 | * @author Kohsuke Kawaguchi 22 | */ 23 | public class UpdateSeniorGroup { 24 | public static void main(String[] args) throws Exception { 25 | Application app = new WebAppMain().createApplication(); 26 | 27 | SearchControls cons = new SearchControls(); 28 | cons.setReturningAttributes(new String[]{"cn", REGISTRATION_DATE}); 29 | 30 | LdapContext con = app.connect(); 31 | try { 32 | NamingEnumeration e = con.search("ou=people,dc=jenkins-ci,dc=org", "(&(objectClass=inetOrgPerson)(!("+SENIOR_STATUS+"=Y)))", cons); 33 | while (e.hasMore()) { 34 | SearchResult r = e.nextElement(); 35 | String cn = (String) r.getAttributes().get("cn").get(); 36 | 37 | System.out.println(cn); 38 | 39 | String dn = r.getNameInNamespace(); 40 | Attribute date = r.getAttributes().get(REGISTRATION_DATE); 41 | if (isQualifiedAsSenior(date)) { 42 | try { 43 | con.modifyAttributes("cn=seniors,ou=groups,dc=jenkins-ci,dc=org", ADD_ATTRIBUTE, new BasicAttributes("member", dn)); 44 | } catch (AttributeInUseException unused) { 45 | // already a member 46 | } 47 | con.modifyAttributes(dn, REPLACE_ATTRIBUTE, new BasicAttributes(SENIOR_STATUS, "Y")); 48 | } else { 49 | System.out.println("\tSkipped"); 50 | } 51 | } 52 | } finally { 53 | con.close(); 54 | } 55 | } 56 | 57 | private static boolean isQualifiedAsSenior(Attribute date) throws Exception { 58 | if (date==null) return true; // older entries do not record this timestamp 59 | 60 | long t = FORMAT.parse((String)date.get()).getTime(); 61 | final long now = System.currentTimeMillis(); 62 | 63 | return now - t > TimeUnit.DAYS.toMillis(1); 64 | } 65 | 66 | private static final SimpleDateFormat FORMAT = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); 67 | } 68 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /src/it/java/org/jenkinsci/account/ui/myaccount/UpdateMyAccountTest.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.account.ui.myaccount; 2 | 3 | import org.jenkinsci.account.ui.BaseTest; 4 | import org.jenkinsci.account.ui.login.LoginPage; 5 | import org.junit.jupiter.api.Test; 6 | import org.openqa.selenium.By; 7 | 8 | import static org.assertj.core.api.Assertions.assertThat; 9 | 10 | public class UpdateMyAccountTest extends BaseTest { 11 | 12 | @Test 13 | public void updateProfileDetails() { 14 | openHomePage(); 15 | 16 | LoginPage loginPage = new LoginPage(driver); 17 | loginPage.login("alice", "password"); 18 | 19 | MyAccountPage myAccountPage = new MyAccountPage(driver); 20 | myAccountPage.clickProfileLink(); 21 | 22 | MyProfilePage profilePage = new MyProfilePage(driver); 23 | profilePage.updateFirstName("Kohsuke1"); 24 | profilePage.updateLastName("Kawaguchi1"); 25 | profilePage.updateEmail("kohsuke@jenkins-ci.org"); 26 | profilePage.updateGitHub("kohsuke1"); 27 | profilePage.updateSSHKeys("abcdefgh"); 28 | profilePage.update(); 29 | 30 | assertThat(driver.findElement(By.tagName("h1")).getText()).isEqualTo("Done!"); 31 | } 32 | 33 | 34 | @Test 35 | public void changePasswordValuesMustMatch() { 36 | openHomePage(); 37 | 38 | LoginPage loginPage = new LoginPage(driver); 39 | loginPage.login("alice", "password"); 40 | 41 | MyAccountPage myAccountPage = new MyAccountPage(driver); 42 | myAccountPage.clickProfileLink(); 43 | 44 | MyProfilePage profilePage = new MyProfilePage(driver); 45 | profilePage.updateOldPassword("password"); 46 | profilePage.updateNewPassword("password1"); 47 | profilePage.updateConfirmNewPassword("password2"); 48 | profilePage.update(); 49 | 50 | String pageTitle = driver.getTitle(); 51 | assertThat(pageTitle).contains("Error"); 52 | } 53 | 54 | @Test 55 | public void changePasswordValuesMustProvideOldPassword() { 56 | openHomePage(); 57 | 58 | LoginPage loginPage = new LoginPage(driver); 59 | loginPage.login("alice", "password"); 60 | 61 | MyAccountPage myAccountPage = new MyAccountPage(driver); 62 | myAccountPage.clickProfileLink(); 63 | 64 | MyProfilePage profilePage = new MyProfilePage(driver); 65 | profilePage.updateNewPassword("password1"); 66 | profilePage.updateConfirmNewPassword("password1"); 67 | profilePage.update(); 68 | 69 | String pageTitle = driver.getTitle(); 70 | assertThat(pageTitle).contains("Error"); 71 | } 72 | 73 | @Test 74 | public void changePassword() { 75 | openHomePage(); 76 | 77 | LoginPage loginPage = new LoginPage(driver); 78 | loginPage.login("alice", "password"); 79 | 80 | MyAccountPage myAccountPage = new MyAccountPage(driver); 81 | myAccountPage.clickProfileLink(); 82 | 83 | MyProfilePage profilePage = new MyProfilePage(driver); 84 | profilePage.changePassword("password", "password1"); 85 | 86 | newSession(); 87 | loginPage = new LoginPage(driver); 88 | loginPage.login("alice", "password1"); 89 | 90 | String pageTitle = driver.getTitle(); 91 | assertThat(pageTitle).contains("Account"); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/account/AdminUI.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.account; 2 | 3 | import jakarta.mail.MessagingException; 4 | import org.jenkinsci.account.Application.User; 5 | import org.kohsuke.stapler.HttpResponse; 6 | import org.kohsuke.stapler.HttpResponses; 7 | import org.kohsuke.stapler.QueryParameter; 8 | import org.kohsuke.stapler.Stapler; 9 | import org.kohsuke.stapler.interceptor.RequirePOST; 10 | 11 | import javax.naming.NamingException; 12 | import javax.naming.ldap.LdapContext; 13 | import javax.servlet.http.HttpServletResponse; 14 | import java.util.ArrayList; 15 | import java.util.Iterator; 16 | import java.util.List; 17 | import java.util.logging.Logger; 18 | 19 | /** 20 | * Root object of the admin UI. 21 | * 22 | * Only administrator gets access to this object. 23 | * 24 | * @author Kohsuke Kawaguchi 25 | */ 26 | public class AdminUI { 27 | private final Application app; 28 | 29 | public AdminUI(Application app) { 30 | this.app = app; 31 | } 32 | 33 | /** 34 | * Exposes the circuit breaker to UI. 35 | */ 36 | public CircuitBreaker getCircuitBreaker() { 37 | return app.circuitBreaker; 38 | } 39 | 40 | public HttpResponse doSearch(@QueryParameter String word) throws NamingException { 41 | List all = new ArrayList(); 42 | LdapContext con = app.connect(); 43 | try { 44 | Iterator itr = app.searchByWord(word, con); 45 | while (itr.hasNext()) 46 | all.add(itr.next()); 47 | 48 | return HttpResponses.forwardToView(this,"search.jelly").with("all",all); 49 | } finally { 50 | con.close(); 51 | } 52 | } 53 | 54 | @RequirePOST 55 | public HttpResponse doPasswordReset(@QueryParameter String id, @QueryParameter String reason) throws NamingException, MessagingException { 56 | LdapContext con = app.connect(); 57 | try { 58 | User u = app.getUserById(id, con); 59 | 60 | String p = PasswordUtil.generateRandomPassword(); 61 | u.modifyPassword(con, p); 62 | u.mailPasswordReset(p, Stapler.getCurrentRequest().getRemoteUser(), reason); 63 | 64 | if (Myself.current().isAuthenticatedWithREST()) { 65 | return HttpResponses.ok(); 66 | } else { 67 | return HttpResponses.forwardToView(this,"newPassword.jelly").with("user", u).with("password", p); 68 | } 69 | } finally { 70 | con.close(); 71 | } 72 | } 73 | 74 | @RequirePOST 75 | public HttpResponse doEmailReset(@QueryParameter String id, @QueryParameter String email) throws NamingException { 76 | LdapContext con = app.connect(); 77 | try { 78 | User u = app.getUserById(id, con); 79 | u.modifyEmail(con, email); 80 | return HttpResponses.redirectTo("."); 81 | } finally { 82 | con.close(); 83 | } 84 | } 85 | 86 | @RequirePOST 87 | public HttpResponse doDelete(@QueryParameter String id, @QueryParameter String confirm) throws NamingException { 88 | if (!confirm.equalsIgnoreCase("YES")) 89 | return HttpResponses.error(HttpServletResponse.SC_BAD_REQUEST,"No confirmation given"); 90 | 91 | LdapContext con = app.connect(); 92 | try { 93 | User u = app.getUserById(id, con); 94 | 95 | u.delete(con); 96 | 97 | return HttpResponses.redirectTo("."); 98 | } finally { 99 | con.close(); 100 | } 101 | } 102 | 103 | @RequirePOST 104 | public HttpResponse doDoSignup( 105 | @QueryParameter String userid, 106 | @QueryParameter String firstName, 107 | @QueryParameter String lastName, 108 | @QueryParameter String email, 109 | @QueryParameter boolean skipPassword, 110 | @QueryParameter String message 111 | ) throws NamingException, MessagingException { 112 | String password = app.createRecord(userid, firstName, lastName, email); 113 | 114 | app.new User(userid,email).mailAccountCreated(!skipPassword, password, message); 115 | 116 | return HttpResponses.plainText("Created"); 117 | } 118 | 119 | private static final Logger LOGGER = Logger.getLogger(AdminUI.class.getName()); 120 | } 121 | -------------------------------------------------------------------------------- /src/it/java/org/jenkinsci/account/ui/BaseTest.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.account.ui; 2 | 3 | import com.icegreen.greenmail.junit5.GreenMailExtension; 4 | import com.icegreen.greenmail.util.ServerSetupTest; 5 | import com.unboundid.ldap.listener.Base64PasswordEncoderOutputFormatter; 6 | import com.unboundid.ldap.listener.InMemoryDirectoryServer; 7 | import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig; 8 | import com.unboundid.ldap.listener.InMemoryListenerConfig; 9 | import com.unboundid.ldap.listener.SaltedMessageDigestInMemoryPasswordEncoder; 10 | import com.unboundid.ldap.sdk.LDAPException; 11 | import io.github.bonigarcia.wdm.WebDriverManager; 12 | import java.io.File; 13 | import java.io.IOException; 14 | import java.lang.reflect.Method; 15 | import java.nio.file.Path; 16 | import java.nio.file.Paths; 17 | import java.security.MessageDigest; 18 | import java.security.NoSuchAlgorithmException; 19 | import java.util.Objects; 20 | import java.util.Optional; 21 | import java.util.UUID; 22 | import org.apache.commons.io.FileUtils; 23 | import org.jenkinsci.account.ui.email.ReadInboundEmailService; 24 | import org.junit.jupiter.api.AfterEach; 25 | import org.junit.jupiter.api.BeforeAll; 26 | import org.junit.jupiter.api.BeforeEach; 27 | import org.junit.jupiter.api.extension.AfterTestExecutionCallback; 28 | import org.junit.jupiter.api.extension.ExtendWith; 29 | import org.junit.jupiter.api.extension.ExtensionContext; 30 | import org.junit.jupiter.api.extension.RegisterExtension; 31 | import org.openqa.selenium.OutputType; 32 | import org.openqa.selenium.TakesScreenshot; 33 | import org.openqa.selenium.chrome.ChromeDriver; 34 | import org.openqa.selenium.chrome.ChromeOptions; 35 | 36 | @ExtendWith(BaseTest.ScreenShotOnFailedTestExtension.class) 37 | public class BaseTest { 38 | 39 | public ChromeDriver driver; 40 | protected InMemoryDirectoryServer ds; 41 | 42 | @RegisterExtension 43 | public static GreenMailExtension greenMail = new GreenMailExtension(ServerSetupTest.SMTP_IMAP); 44 | 45 | public static final ReadInboundEmailService READ_INBOUND_EMAIL_SERVICE = new ReadInboundEmailService("localhost", 3143); 46 | 47 | @BeforeAll 48 | static void setupAll() { 49 | WebDriverManager.chromiumdriver().setup(); 50 | } 51 | 52 | @BeforeEach 53 | void before() throws LDAPException, NoSuchAlgorithmException { 54 | InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig("dc=jenkins-ci,dc=org"); 55 | config.setPasswordEncoders( 56 | new SaltedMessageDigestInMemoryPasswordEncoder("{SSHA}", 57 | Base64PasswordEncoderOutputFormatter.getInstance(), 58 | MessageDigest.getInstance("SHA-1"), 59 | 4, 60 | true, 61 | true 62 | )); 63 | config.addAdditionalBindCredentials("cn=Directory Manager", "password"); 64 | 65 | InMemoryListenerConfig listener = InMemoryListenerConfig.createLDAPConfig("default", 3389); 66 | config.setListenerConfigs(listener); 67 | 68 | ds = new InMemoryDirectoryServer(config); 69 | 70 | // I wasn't able to load this as a resource for some reason, gradle not setting up resources properly maybe 71 | Path resource = Paths.get("src", "it", "resources", "org", "jenkinsci", "account", "ui", "test-data.ldif"); 72 | Objects.requireNonNull(resource); 73 | 74 | ds.importFromLDIF(true, resource.toFile()); 75 | ds.startListening(); 76 | 77 | startBrowser(); 78 | } 79 | 80 | public void startBrowser() { 81 | ChromeOptions options = new ChromeOptions(); 82 | options.addArguments("--headless", "--window-size=1920,1080"); 83 | driver = new ChromeDriver(options); 84 | } 85 | 86 | public void openHomePage() { 87 | driver.get(System.getProperty("gretty.httpBaseURI")); 88 | } 89 | 90 | public void newSession() { 91 | driver.quit(); 92 | startBrowser(); 93 | openHomePage(); 94 | } 95 | 96 | /** 97 | * Useful to use when you want to debug a test interactively 98 | */ 99 | @SuppressWarnings({"unused", "InfiniteLoopStatement", "BusyWait"}) 100 | public void pause() throws InterruptedException { 101 | while (true) { 102 | Thread.sleep(2000L); 103 | } 104 | } 105 | 106 | public void takeScreenshot(String testName) { 107 | File scrFile = ((TakesScreenshot)driver).getScreenshotAs(OutputType.FILE); 108 | try { 109 | FileUtils.copyFile( 110 | scrFile, 111 | new File(String.format("errorScreenshots/%s-%s.jpg", testName, UUID.randomUUID()) 112 | ) 113 | ); 114 | } catch (IOException e) { 115 | throw new RuntimeException(e); 116 | } 117 | } 118 | 119 | @AfterEach 120 | public void after() { 121 | if (driver != null) { 122 | driver.quit(); 123 | } 124 | 125 | if (ds != null) { 126 | ds.close(); 127 | } 128 | } 129 | 130 | public static class ScreenShotOnFailedTestExtension implements AfterTestExecutionCallback { 131 | 132 | @Override 133 | public void afterTestExecution(ExtensionContext context) throws Exception { 134 | boolean testFailed = context.getExecutionException().isPresent(); 135 | 136 | if (testFailed) { 137 | BaseTest baseTest = (BaseTest) context.getRequiredTestInstance(); 138 | Optional testMethod = context.getTestMethod(); 139 | 140 | String displayName = testMethod 141 | .map(Method::getName).orElse(context 142 | .getDisplayName() 143 | .replace("(", "") 144 | .replace(")", "")); 145 | 146 | baseTest.takeScreenshot(displayName); 147 | } 148 | 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/main/resources/org/jenkinsci/account/taglib/layout.jelly: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | ${attrs.title} | Jenkins 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 |
51 | 52 | 83 | 84 | 85 |
86 | 87 |
88 |
89 | 90 | 91 | 93 | 104 | 105 | 116 | 117 | 118 | 119 |
120 | -------------------------------------------------------------------------------- /src/main/webapp/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --bs-navbar-brand-color: var(--color) !important; 3 | } 4 | 5 | .ac-description { 6 | color: var(--color--secondary); 7 | } 8 | 9 | .ac-form-group { 10 | display: flex; 11 | flex-direction: column; 12 | } 13 | 14 | .ac-sidepanel-layout { 15 | margin-inline: auto; 16 | display: inline-grid; 17 | grid-template-columns: 280px 840px; 18 | gap: 3.75rem; 19 | width: unset; 20 | 21 | h1 { 22 | font-size: 1.8rem; 23 | font-weight: 600; 24 | } 25 | 26 | h2 { 27 | font-size: 1.4rem; 28 | font-weight: 600; 29 | } 30 | } 31 | 32 | input[type='text'], input[type='password'], input[type='email'], textarea { 33 | --background: color-mix(in srgb, var(--color) 1%, transparent); 34 | --border: color-mix(in srgb, var(--color) 5%, transparent); 35 | 36 | appearance: none; 37 | font-size: 1rem; 38 | background: var(--background); 39 | border: 2px solid var(--border); 40 | padding: 0.6rem 0.7rem; 41 | border-radius: 0.66rem; 42 | color: var(--color); 43 | transition: 0.2s ease; 44 | box-shadow: 0 0 0 10px transparent; 45 | outline: none; 46 | 47 | &:not(:disabled) { 48 | &:hover { 49 | --background: color-mix(in srgb, var(--color) 5%, transparent); 50 | --border: color-mix(in srgb, var(--color) 10%, transparent); 51 | } 52 | 53 | &:active, &:focus { 54 | --background: color-mix(in srgb, var(--color) 10%, transparent); 55 | --border: var(--accent-color); 56 | box-shadow: 0 0 0 5px color-mix(in srgb, var(--accent-color) 20%, transparent); 57 | } 58 | } 59 | 60 | &:disabled { 61 | opacity: 0.5; 62 | } 63 | } 64 | 65 | .ac-small-width { 66 | max-width: 550px; 67 | display: flex; 68 | flex-direction: column; 69 | gap: 1.5rem; 70 | 71 | form, .ac-main-content { 72 | display: contents; 73 | } 74 | 75 | p { 76 | margin: 0; 77 | } 78 | } 79 | 80 | h1 { 81 | margin: 0; 82 | } 83 | 84 | .ac-button--large { 85 | display: flex !important; 86 | padding: 0.75rem !important; 87 | font-size: 1rem !important; 88 | width: 100%; 89 | cursor: pointer; 90 | } 91 | 92 | .ac-main-content { 93 | display: flex; 94 | flex-direction: column; 95 | gap: 1.5rem; 96 | font-size: 1rem; 97 | 98 | form { 99 | display: contents; 100 | } 101 | 102 | h1, h2, p { 103 | margin: 0; 104 | } 105 | } 106 | 107 | .ac-navbar { 108 | display: flex; 109 | flex-direction: column; 110 | gap: 0.125rem; 111 | 112 | .h1 { 113 | margin-top: 0; 114 | margin-bottom: 1.5rem; 115 | margin-left: 0.85rem; 116 | font-size: 1.8rem; 117 | font-weight: 600; 118 | } 119 | 120 | ul { 121 | display: contents; 122 | 123 | li { 124 | display: contents; 125 | 126 | a { 127 | display: flex; 128 | align-items: center; 129 | justify-content: start; 130 | gap: 0.65rem; 131 | color: var(--color); 132 | border-radius: 0.66rem; 133 | padding: 0.65rem 0.85rem; 134 | text-decoration: none; 135 | line-height: 1; 136 | font-size: 0.865rem; 137 | font-weight: 450; 138 | transition: 0.2s ease; 139 | 140 | ion-icon { 141 | font-size: 1.125rem; 142 | } 143 | 144 | &:hover { 145 | background: color-mix(in srgb, var(--color--secondary) 5%, transparent); 146 | } 147 | 148 | &:active, &:focus, &.active { 149 | background: color-mix(in srgb, var(--color--secondary) 10%, transparent); 150 | font-weight: 500; 151 | } 152 | } 153 | } 154 | } 155 | } 156 | 157 | .ac-login__controls { 158 | display: flex; 159 | align-items: center; 160 | justify-content: center; 161 | gap: 1rem; 162 | } 163 | 164 | .ac-table { 165 | --table-background: color-mix(in srgb, var(--color--secondary) 5%, transparent); 166 | --table-border-radius: 0.75rem; 167 | --table-row-border-radius: 0.25rem; 168 | --table-padding: 0.45rem; 169 | 170 | position: relative; 171 | width: 100%; 172 | background: var(--table-background); 173 | border-radius: calc(var(--table-border-radius) + 4px); 174 | border: 4px solid var(--table-background); 175 | border-bottom-width: 2px; 176 | border-spacing: 0 2px; 177 | background-clip: padding-box; 178 | 179 | * { 180 | -webkit-border-horizontal-spacing: 0; 181 | -webkit-border-vertical-spacing: 0; 182 | } 183 | 184 | & > thead { 185 | & > tr { 186 | & > th { 187 | color: var(--color); 188 | text-align: left; 189 | padding-top: calc(var(--table-padding) * 0.9); 190 | padding-bottom: calc(var(--table-padding) * 1.3); 191 | padding-left: 1.25rem; 192 | font-weight: 500; 193 | font-size: 0.875rem; 194 | 195 | &:first-of-type { 196 | padding-left: calc(var(--table-padding) * 2); 197 | } 198 | 199 | &:last-of-type { 200 | padding-right: var(--table-padding); 201 | } 202 | } 203 | } 204 | } 205 | 206 | & > tbody { 207 | & > tr { 208 | color: var(--color); 209 | 210 | & > td { 211 | background: var(--background); 212 | vertical-align: middle; 213 | padding: var(--table-padding) 0 var(--table-padding) 1.25rem; 214 | height: 3rem; 215 | 216 | &:first-of-type { 217 | padding-left: calc(var(--table-padding) * 2); 218 | } 219 | 220 | &:last-of-type { 221 | padding-right: var(--table-padding); 222 | } 223 | } 224 | 225 | & > td:first-of-type { 226 | border-top-left-radius: var(--table-row-border-radius); 227 | border-bottom-left-radius: var(--table-row-border-radius); 228 | } 229 | 230 | & > td:last-of-type { 231 | border-top-right-radius: var(--table-row-border-radius); 232 | border-bottom-right-radius: var(--table-row-border-radius); 233 | } 234 | 235 | &:first-of-type { 236 | & > td:first-of-type { 237 | border-top-left-radius: var(--table-border-radius); 238 | } 239 | 240 | & > td:last-of-type { 241 | border-top-right-radius: var(--table-border-radius); 242 | } 243 | } 244 | 245 | &:last-of-type { 246 | & > td:first-of-type { 247 | border-bottom-left-radius: var(--table-border-radius); 248 | } 249 | 250 | & > td:last-of-type { 251 | border-bottom-right-radius: var(--table-border-radius); 252 | } 253 | } 254 | } 255 | } 256 | } 257 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/account/Myself.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.account; 2 | 3 | import org.kohsuke.stapler.HttpRedirect; 4 | import org.kohsuke.stapler.HttpResponse; 5 | import org.kohsuke.stapler.QueryParameter; 6 | import org.kohsuke.stapler.Stapler; 7 | 8 | import javax.naming.NamingException; 9 | import javax.naming.directory.Attribute; 10 | import javax.naming.directory.Attributes; 11 | import javax.naming.directory.BasicAttributes; 12 | import javax.naming.directory.DirContext; 13 | import javax.naming.ldap.LdapContext; 14 | import java.util.Set; 15 | import java.util.logging.Logger; 16 | 17 | import static org.jenkinsci.account.LdapAbuse.GITHUB_ID; 18 | import static org.jenkinsci.account.LdapAbuse.SSH_KEYS; 19 | import static org.jenkinsci.account.LdapAbuse.REGISTRATION_DATE; 20 | 21 | /** 22 | * Represents the current user logged in and operations on it. 23 | * 24 | * @author Kohsuke Kawaguchi 25 | * @see Application#getMyself() 26 | */ 27 | public class Myself { 28 | private final Application parent; 29 | private final String dn; 30 | public String firstName, lastName, email, userId; 31 | public String githubId, sshKeys, registrationDate; 32 | /** 33 | * Indicates that a REST API client is used. 34 | * It will impact some redirects on commands. 35 | */ 36 | private boolean authenticatedWithREST; 37 | private final Set groups; 38 | 39 | public Myself(Application parent, String dn, Attributes attributes, Set groups) throws NamingException { 40 | this(parent, dn, 41 | getAttribute(attributes,"givenName"), 42 | getAttribute(attributes,"sn"), 43 | getAttribute(attributes,"mail"), 44 | getAttribute(attributes,"cn"), 45 | getAttribute(attributes, REGISTRATION_DATE), 46 | getAttribute(attributes, GITHUB_ID), 47 | getAttribute(attributes, SSH_KEYS), 48 | groups); 49 | } 50 | 51 | public Myself(Application parent, String dn, String firstName, String lastName, String email, String userId, String registrationDate, String githubId, String sshKeys, Set groups) { 52 | this.parent = parent; 53 | this.dn = dn; 54 | this.firstName = firstName; 55 | this.lastName = lastName; 56 | this.email = email; 57 | this.userId = userId; 58 | this.githubId = githubId; 59 | this.sshKeys = sshKeys; 60 | this.groups = groups; 61 | this.registrationDate = registrationDate; 62 | } 63 | 64 | public static Myself current() { 65 | return (Myself) Stapler.getCurrentRequest().getSession().getAttribute(Myself.class.getName()); 66 | } 67 | 68 | /** 69 | * Is this an admin user? 70 | */ 71 | public boolean isAdmin() { 72 | return groups.contains("admins"); 73 | } 74 | 75 | public Myself withRESTAuthentication(boolean value) { 76 | this.authenticatedWithREST = value; 77 | return this; 78 | } 79 | 80 | public boolean isAuthenticatedWithREST() { 81 | return this.authenticatedWithREST; 82 | } 83 | 84 | private static String getAttribute(Attributes attributes, String name) throws NamingException { 85 | Attribute att = attributes.get(name); 86 | return att!=null ? (String) att.get() : null; 87 | } 88 | 89 | public HttpResponse doUpdate( 90 | @QueryParameter String firstName, 91 | @QueryParameter String lastName, 92 | @QueryParameter String email, 93 | @QueryParameter String githubId, 94 | @QueryParameter String sshKeys, 95 | @QueryParameter String password, 96 | @QueryParameter String newPassword1, 97 | @QueryParameter String newPassword2 98 | ) throws Exception { 99 | 100 | if (!isValidName(firstName)) { 101 | throw new UserError("First name is Invalid"); 102 | } 103 | if (!isValidName(lastName)) { 104 | throw new UserError("Last name is Invalid"); 105 | } 106 | final Attributes attrs = new BasicAttributes(); 107 | 108 | attrs.put("givenName", firstName); 109 | attrs.put("sn", lastName); 110 | attrs.put("mail", email); 111 | attrs.put(GITHUB_ID,fixEmpty(githubId)); 112 | attrs.put(SSH_KEYS,fixEmpty(sshKeys)); // hack since I find it too hard to add custom attributes to LDAP 113 | 114 | LdapContext context = parent.connect(); 115 | try { 116 | context.modifyAttributes(dn,DirContext.REPLACE_ATTRIBUTE,attrs); 117 | } finally { 118 | context.close(); 119 | } 120 | 121 | this.firstName = firstName; 122 | this.lastName = lastName; 123 | this.email = email; 124 | this.githubId = githubId; 125 | this.sshKeys = sshKeys; 126 | 127 | LOGGER.info("User "+userId+" updated the profile. email="+email); 128 | 129 | if (fixEmpty(password) != null || fixEmpty(newPassword1) != null || fixEmpty(newPassword2) != null) { 130 | return doChangePassword(password, newPassword1, newPassword2); 131 | } 132 | 133 | return new HttpRedirect("done"); 134 | } 135 | 136 | // no longer invoked directly from outside, but left as is 137 | public HttpResponse doChangePassword( 138 | @QueryParameter String password, 139 | @QueryParameter String newPassword1, 140 | @QueryParameter String newPassword2 141 | ) throws Exception { 142 | 143 | if (fixEmpty(password) == null) 144 | throw new UserError("Current password is empty"); 145 | 146 | if (fixEmpty(newPassword1) == null || fixEmpty(newPassword2) == null) 147 | throw new UserError("New password is empty"); 148 | 149 | // verify if new password match 150 | if (!newPassword1.equals(newPassword2)) 151 | throw new UserError("Password does not match the confirm password"); 152 | 153 | try { 154 | parent.connect(dn,password).close(); 155 | } catch (javax.naming.AuthenticationException e) { 156 | throw new UserError("Wrong current password"); 157 | } 158 | 159 | // then update 160 | Attributes attrs = new BasicAttributes(); 161 | attrs.put("userPassword", PasswordUtil.hashPassword(newPassword1)); 162 | 163 | LdapContext context = parent.connect(); 164 | try { 165 | context.modifyAttributes(dn,DirContext.REPLACE_ATTRIBUTE,attrs); 166 | } finally { 167 | context.close(); 168 | } 169 | 170 | LOGGER.info("User "+userId+" changed the password"); 171 | 172 | return new HttpRedirect("done"); 173 | } 174 | 175 | private String fixEmpty(String s) { 176 | if (s!=null && s.length()==0) return null; 177 | return s; 178 | } 179 | 180 | private boolean isValidName(String name) { 181 | return name != null && !name.isEmpty() && name.length() < 100; 182 | } 183 | 184 | private static final Logger LOGGER = Logger.getLogger(Myself.class.getName()); 185 | } 186 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s 90 | ' "$PWD" ) || exit 91 | 92 | # Use the maximum available, or set MAX_FD != -1 to use that value. 93 | MAX_FD=maximum 94 | 95 | warn () { 96 | echo "$*" 97 | } >&2 98 | 99 | die () { 100 | echo 101 | echo "$*" 102 | echo 103 | exit 1 104 | } >&2 105 | 106 | # OS specific support (must be 'true' or 'false'). 107 | cygwin=false 108 | msys=false 109 | darwin=false 110 | nonstop=false 111 | case "$( uname )" in #( 112 | CYGWIN* ) cygwin=true ;; #( 113 | Darwin* ) darwin=true ;; #( 114 | MSYS* | MINGW* ) msys=true ;; #( 115 | NONSTOP* ) nonstop=true ;; 116 | esac 117 | 118 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 119 | 120 | 121 | # Determine the Java command to use to start the JVM. 122 | if [ -n "$JAVA_HOME" ] ; then 123 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 124 | # IBM's JDK on AIX uses strange locations for the executables 125 | JAVACMD=$JAVA_HOME/jre/sh/java 126 | else 127 | JAVACMD=$JAVA_HOME/bin/java 128 | fi 129 | if [ ! -x "$JAVACMD" ] ; then 130 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 131 | 132 | Please set the JAVA_HOME variable in your environment to match the 133 | location of your Java installation." 134 | fi 135 | else 136 | JAVACMD=java 137 | if ! command -v java >/dev/null 2>&1 138 | then 139 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 140 | 141 | Please set the JAVA_HOME variable in your environment to match the 142 | location of your Java installation." 143 | fi 144 | fi 145 | 146 | # Increase the maximum file descriptors if we can. 147 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 148 | case $MAX_FD in #( 149 | max*) 150 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 151 | # shellcheck disable=SC2039,SC3045 152 | MAX_FD=$( ulimit -H -n ) || 153 | warn "Could not query maximum file descriptor limit" 154 | esac 155 | case $MAX_FD in #( 156 | '' | soft) :;; #( 157 | *) 158 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 159 | # shellcheck disable=SC2039,SC3045 160 | ulimit -n "$MAX_FD" || 161 | warn "Could not set maximum file descriptor limit to $MAX_FD" 162 | esac 163 | fi 164 | 165 | # Collect all arguments for the java command, stacking in reverse order: 166 | # * args from the command line 167 | # * the main class name 168 | # * -classpath 169 | # * -D...appname settings 170 | # * --module-path (only if needed) 171 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 172 | 173 | # For Cygwin or MSYS, switch paths to Windows format before running java 174 | if "$cygwin" || "$msys" ; then 175 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 176 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 177 | 178 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 179 | 180 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 181 | for arg do 182 | if 183 | case $arg in #( 184 | -*) false ;; # don't mess with options #( 185 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 186 | [ -e "$t" ] ;; #( 187 | *) false ;; 188 | esac 189 | then 190 | arg=$( cygpath --path --ignore --mixed "$arg" ) 191 | fi 192 | # Roll the args list around exactly as many times as the number of 193 | # args, so each arg winds up back in the position where it started, but 194 | # possibly modified. 195 | # 196 | # NB: a `for` loop captures its iteration list before it begins, so 197 | # changing the positional parameters here affects neither the number of 198 | # iterations, nor the values presented in `arg`. 199 | shift # remove old arg 200 | set -- "$@" "$arg" # push replacement arg 201 | done 202 | fi 203 | 204 | 205 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 206 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 207 | 208 | # Collect all arguments for the java command: 209 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 210 | # and any embedded shellness will be escaped. 211 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 212 | # treated as '${Hostname}' itself on the command line. 213 | 214 | set -- \ 215 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 216 | -classpath "$CLASSPATH" \ 217 | org.gradle.wrapper.GradleWrapperMain \ 218 | "$@" 219 | 220 | # Stop when "xargs" is not available. 221 | if ! command -v xargs >/dev/null 2>&1 222 | then 223 | die "xargs is not available" 224 | fi 225 | 226 | # Use "xargs" to parse quoted args. 227 | # 228 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 229 | # 230 | # In Bash we could simply go: 231 | # 232 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 233 | # set -- "${ARGS[@]}" "$@" 234 | # 235 | # but POSIX shell has neither arrays nor command substitution, so instead we 236 | # post-process each arg (as a line of input to sed) to backslash-escape any 237 | # character that might be a shell metacharacter, then use eval to reverse 238 | # that process (while maintaining the separation between arguments), and wrap 239 | # the whole thing up as a single "set" statement. 240 | # 241 | # This will of course break if any of these variables contains a newline or 242 | # an unmatched quote. 243 | # 244 | 245 | eval "set -- $( 246 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 247 | xargs -n1 | 248 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 249 | tr '\n' ' ' 250 | )" '"$@"' 251 | 252 | exec "$JAVACMD" "$@" 253 | -------------------------------------------------------------------------------- /src/main/java/org/jenkinsci/account/Application.java: -------------------------------------------------------------------------------- 1 | package org.jenkinsci.account; 2 | 3 | import com.captcha.botdetect.web.servlet.Captcha; 4 | import com.google.common.net.InetAddresses; 5 | import jakarta.mail.Authenticator; 6 | import jakarta.mail.Message; 7 | import jakarta.mail.MessagingException; 8 | import jakarta.mail.PasswordAuthentication; 9 | import jakarta.mail.Session; 10 | import jakarta.mail.Transport; 11 | import jakarta.mail.internet.InternetAddress; 12 | import jakarta.mail.internet.MimeMessage; 13 | import java.io.UnsupportedEncodingException; 14 | import java.net.URLEncoder; 15 | import java.text.SimpleDateFormat; 16 | import java.util.ArrayList; 17 | import java.util.Arrays; 18 | import java.util.Base64; 19 | import java.util.Date; 20 | import java.util.Enumeration; 21 | import java.util.HashSet; 22 | import java.util.Hashtable; 23 | import java.util.Iterator; 24 | import java.util.List; 25 | import java.util.Locale; 26 | import java.util.Properties; 27 | import java.util.Set; 28 | import java.util.logging.Level; 29 | import java.util.logging.Logger; 30 | import java.util.regex.Pattern; 31 | import javax.annotation.CheckForNull; 32 | import javax.naming.AuthenticationException; 33 | import javax.naming.Context; 34 | import javax.naming.NameAlreadyBoundException; 35 | import javax.naming.NamingEnumeration; 36 | import javax.naming.NamingException; 37 | import javax.naming.directory.AttributeInUseException; 38 | import javax.naming.directory.Attributes; 39 | import javax.naming.directory.BasicAttributes; 40 | import javax.naming.directory.DirContext; 41 | import javax.naming.directory.SearchControls; 42 | import javax.naming.directory.SearchResult; 43 | import javax.naming.ldap.InitialLdapContext; 44 | import javax.naming.ldap.LdapContext; 45 | import javax.servlet.http.Cookie; 46 | import org.apache.commons.lang.StringUtils; 47 | import org.jenkinsci.account.openid.JenkinsOpenIDServer; 48 | import org.kohsuke.stapler.Header; 49 | import org.kohsuke.stapler.HttpRedirect; 50 | import org.kohsuke.stapler.HttpResponse; 51 | import org.kohsuke.stapler.HttpResponses; 52 | import org.kohsuke.stapler.QueryParameter; 53 | import org.kohsuke.stapler.Stapler; 54 | import org.kohsuke.stapler.StaplerRequest; 55 | import org.kohsuke.stapler.StaplerResponse; 56 | 57 | import static javax.naming.directory.DirContext.ADD_ATTRIBUTE; 58 | import static javax.naming.directory.DirContext.REMOVE_ATTRIBUTE; 59 | import static javax.naming.directory.DirContext.REPLACE_ATTRIBUTE; 60 | import static javax.naming.directory.SearchControls.SUBTREE_SCOPE; 61 | import static org.apache.commons.lang.StringUtils.isEmpty; 62 | import static org.jenkinsci.account.LdapAbuse.REGISTRATION_DATE; 63 | import static org.jenkinsci.account.LdapAbuse.SENIOR_STATUS; 64 | 65 | /** 66 | * Root of the account application. 67 | * 68 | * @author Kohsuke Kawaguchi 69 | */ 70 | public class Application { 71 | /** 72 | * Configuration parameter. 73 | */ 74 | private final Parameters params; 75 | 76 | /** 77 | * For bringing the OpenID server into the URL space. 78 | */ 79 | public final JenkinsOpenIDServer openid; 80 | 81 | // not exposing this to UI 82 | /*package*/ final CircuitBreaker circuitBreaker; 83 | 84 | public Application(Parameters params) throws Exception { 85 | this.params = params; 86 | this.openid = new JenkinsOpenIDServer(this); 87 | this.circuitBreaker = new CircuitBreaker(params); 88 | } 89 | 90 | public String getUrl() { 91 | return params.url(); 92 | } 93 | 94 | public String showCaptcha(String name){ 95 | Captcha captcha = Captcha.load(Stapler.getCurrentRequest(), name); 96 | captcha.setUserInputID("captchaCode"); 97 | return captcha.getHtml(); 98 | } 99 | 100 | /** 101 | * Receives the sign-up form submission. 102 | */ 103 | public HttpResponse doDoSignup( 104 | StaplerRequest request, 105 | StaplerResponse response, 106 | @QueryParameter String userid, 107 | @QueryParameter String firstName, 108 | @QueryParameter String lastName, 109 | @QueryParameter String email, 110 | @QueryParameter String emailconfirm, 111 | @QueryParameter String hp, 112 | @QueryParameter String captchaCode, 113 | @Header("X-Forwarded-For") String ip // client IP 114 | ) throws Exception { 115 | 116 | ip = extractFirst(ip); 117 | 118 | Captcha captcha = Captcha.load(request, "signUpCaptcha"); 119 | boolean isHuman = captcha.validate(request.getParameter("captchaCode")); 120 | 121 | // Check if userid and email are available before going further 122 | final DirContext con = connect(); 123 | try { 124 | if (userid == null || isEmpty(userid)) 125 | throw new UserError("UserId is required"); 126 | 127 | userid = userid.toLowerCase(); 128 | 129 | Iterator a = searchByWord(userid, con); 130 | if (a.hasNext()) 131 | throw new UserError("account "+ getUserById(userid,con).id + " already exist"); 132 | a = searchByWord(email, con); 133 | 134 | if (a.hasNext()) 135 | throw new UserError("email: "+ email + " already exist"); 136 | 137 | } finally { 138 | con.close(); 139 | } 140 | 141 | if (!VALID_ID.matcher(userid).matches()) 142 | throw new UserError("Invalid user name: "+userid); 143 | if (isEmpty(firstName)) 144 | throw new UserError("First name is required"); 145 | if (isEmpty(lastName)) 146 | throw new UserError("Last name is required"); 147 | if (isEmpty(email)) 148 | throw new UserError("email is required"); 149 | if (!email.equals(emailconfirm)) 150 | throw new UserError(String.format("Following emails are not matching: %s - %s", email, emailconfirm)); 151 | if(!email.contains("@")) 152 | throw new UserError("Need a valid email address."); 153 | if (!isHuman) 154 | throw new UserError("Captcha mismatch. Please try again and retry a captcha to prove that you are a human"); 155 | 156 | List blockReasons = new ArrayList(); 157 | 158 | if(!isEmpty(hp)) 159 | blockReasons.add("Honeypot: " + hp); 160 | 161 | if(Pattern.matches("(?:^jb\\d+@gmail.com|seo\\d+@)", email)) { 162 | blockReasons.add("BL: email (custom)"); 163 | } 164 | 165 | if( 166 | (firstName.equalsIgnoreCase("help") && lastName.equalsIgnoreCase("desk")) || 167 | (firstName.contains("quickbook") || lastName.contains("quickbook")) 168 | ) { 169 | blockReasons.add("BL: name (custom)"); 170 | } 171 | 172 | for (String fragment : USERID_BLACKLIST) { 173 | if(userid.toLowerCase().contains(fragment.toLowerCase())) { 174 | blockReasons.add("BL: userid"); 175 | } 176 | } 177 | 178 | for (String fragment : IP_BLACKLIST) { 179 | if(ip.startsWith(fragment)) { 180 | blockReasons.add("BL: IP"); 181 | } 182 | } 183 | // domain black list 184 | String lm = email.toLowerCase(Locale.ENGLISH); 185 | for (String fragment : EMAIL_BLACKLIST) { 186 | if (lm.contains(fragment.toLowerCase())) 187 | blockReasons.add("BL: email"); 188 | } 189 | 190 | for(String fragment : NAUGHTY_BLACKLIST) { 191 | if(userid.toLowerCase().contains(fragment.toLowerCase()) 192 | || firstName.toLowerCase().contains(fragment.toLowerCase()) 193 | || lastName.toLowerCase().contains(fragment.toLowerCase()) 194 | ) { 195 | blockReasons.add("BL: naughty"); 196 | } 197 | } 198 | 199 | if(checkCookie(request, ALREADY_SIGNED_UP)) { 200 | blockReasons.add("Cookie"); 201 | } 202 | 203 | if(circuitBreaker.check()) { 204 | blockReasons.add("circuit breaker"); 205 | } 206 | 207 | String userDetails = userDetails(userid, firstName, lastName, email, ip); 208 | if(!blockReasons.isEmpty()) { 209 | String body = "Rejecting, likely spam:\n\n" + userDetails + "\n\nHTTP Headers\n" + 210 | dumpHeaders(request) + "\n\n" + 211 | "===Block Reasons===\n" 212 | + String.join("\n", blockReasons) + "\n===================" + "\n\n" + 213 | "IP Void link: http://ipvoid.com/scan/" + ip + "\n\n" + 214 | "To allow this account to be created, click the following link:\n" + 215 | getUrl() + "/admin/signup?userId=" + enc(userid) + "&firstName=" + enc(firstName) + "&lastName=" + enc(lastName) + "&email=" + enc(email) + "\n"; 216 | LOGGER.warning(userDetails.replaceAll("\\R", " ") + " signup rejected, likely spam: " + String.join(" / ", blockReasons)); 217 | mail("jenkinsci-account-admins@googlegroups.com", "Rejection of a new account creation for " + firstName + " " + lastName, body, "text/plain"); 218 | throw new SystemError(SPAM_MESSAGE); 219 | } 220 | 221 | String password = createRecord(userid, firstName, lastName, email); 222 | LOGGER.info("User "+userid+" is from "+ip); 223 | mail("jenkinsci-account-admins@googlegroups.com", "New user created for " + userid, 224 | userDetails + "\n\nHTTP Headers\n" + 225 | dumpHeaders(request) + "\n\n" + "Account page: " + getUrl() + "/admin/search?word=" + userid + "\n\nIP Void link: http://ipvoid.com/scan/" + ip + "/\n", "text/plain"); 226 | new User(userid,email).mailPassword(password); 227 | 228 | Cookie cookie = new Cookie(ALREADY_SIGNED_UP, "1"); 229 | cookie.setDomain("accounts.jenkins.io"); 230 | cookie.setPath("/"); 231 | cookie.setMaxAge(24 * 60 * 60); 232 | response.addCookie(cookie); 233 | 234 | return new HttpRedirect("doneMail"); 235 | } 236 | 237 | private String dumpHeaders(StaplerRequest request) { 238 | Enumeration headerNames = request.getHeaderNames(); 239 | StringBuffer buffer = new StringBuffer(); 240 | while(headerNames.hasMoreElements()) { 241 | String headerName = (String)headerNames.nextElement(); 242 | buffer.append(headerName).append("=").append(request.getHeader(headerName)).append("\n"); 243 | } 244 | return buffer.toString(); 245 | } 246 | 247 | private boolean checkCookie(StaplerRequest request, String x) { 248 | for (Cookie cookie: request.getCookies()) { 249 | if(cookie.getName().equals(ALREADY_SIGNED_UP)) { 250 | return "1".equals(cookie.getValue()); 251 | } 252 | } 253 | return false; 254 | } 255 | 256 | private void mail(String to, String subject, String body, String encoding) throws MessagingException { 257 | Session s = createJavaMailSession(); 258 | MimeMessage msg = new MimeMessage(s); 259 | msg.setSubject(subject); 260 | msg.setFrom(new InternetAddress(String.format("Jenkins Accounts <%s>", params.smtpSender()))); 261 | msg.setRecipient(Message.RecipientType.TO, new InternetAddress(to)); 262 | msg.setContent(body, encoding); 263 | Transport.send(msg); 264 | } 265 | 266 | private String userDetails(String userid, String firstName, String lastName, String email, String ip) { 267 | return String.format( 268 | "ip=%s\nemail=%s\nuserId=%s\nlastName=%s\nfirstName=%s", 269 | ip, email, userid, lastName, firstName); 270 | } 271 | 272 | /** 273 | * If IP consists of multiple tokens, like "1.2.3.4, 5.6.7.8" then just extract the first one. 274 | */ 275 | private String extractFirst(String ip) { 276 | if (ip==null) return "127.0.0.1"; 277 | int idx = ip.indexOf(","); 278 | if(idx>0) { 279 | for (String xForwardedFor : ip.split(",")) { 280 | if (StringUtils.isNotBlank(xForwardedFor) && !InetAddresses.forString(xForwardedFor).isSiteLocalAddress()) { 281 | return ip; 282 | } 283 | } 284 | } 285 | return ip; 286 | } 287 | 288 | private static String enc(String s) throws UnsupportedEncodingException { 289 | return URLEncoder.encode(s,"UTF-8"); 290 | } 291 | 292 | /** 293 | * Adds the new user entry to LDAP. 294 | */ 295 | public String createRecord(String userid, String firstName, String lastName, String email) throws NamingException { 296 | Attributes attrs = new BasicAttributes(); 297 | attrs.put("objectClass", "inetOrgPerson"); 298 | attrs.put("givenName", firstName); 299 | attrs.put("sn", lastName); 300 | attrs.put("mail", email); 301 | String password = PasswordUtil.generateRandomPassword(); 302 | attrs.put("userPassword", PasswordUtil.hashPassword(password)); 303 | attrs.put(REGISTRATION_DATE, new SimpleDateFormat("yyyy/MM/dd HH:mm:ss").format(new Date())); 304 | attrs.put(SENIOR_STATUS, "N"); 305 | 306 | final DirContext con = connect(); 307 | try { 308 | String fullDN = "cn=" + userid + "," + params.newUserBaseDN(); 309 | con.createSubcontext(fullDN, attrs).close(); 310 | 311 | // add to the right group 312 | try { 313 | con.modifyAttributes("cn=all,ou=groups,dc=jenkins-ci,dc=org",ADD_ATTRIBUTE,new BasicAttributes("member",fullDN)); 314 | } catch (AttributeInUseException e) { 315 | // deletes and re-add it to make the case match 316 | con.modifyAttributes("cn=all,ou=groups,dc=jenkins-ci,dc=org",REMOVE_ATTRIBUTE,new BasicAttributes("member",fullDN)); 317 | con.modifyAttributes("cn=all,ou=groups,dc=jenkins-ci,dc=org",ADD_ATTRIBUTE,new BasicAttributes("member",fullDN)); 318 | } 319 | } catch (NameAlreadyBoundException e) { 320 | throw new UserError("ID "+userid+" is already taken. Perhaps you already have an account imported from legacy java.net? You may try resetting the password."); 321 | } finally { 322 | con.close(); 323 | } 324 | 325 | LOGGER.info("User "+userid+" signed up: "+email); 326 | return password; 327 | } 328 | 329 | /** 330 | * Handles the password reset form submission. 331 | */ 332 | public HttpResponse doDoPasswordReset(@QueryParameter String id, @QueryParameter String reason) throws Exception { 333 | final DirContext con = connect(); 334 | if (id.isEmpty()) 335 | throw new UserError("No email or user account provided"); 336 | try { 337 | Iterator a = searchByWord(id, con); 338 | if (a.hasNext()) { 339 | User u = a.next(); 340 | 341 | String p = PasswordUtil.generateRandomPassword(); 342 | u.modifyPassword(con, p); 343 | u.mailPasswordReset(p, Stapler.getCurrentRequest().getRemoteUser(), 344 | StringUtils.isBlank(reason) ? "request in the account management app" : reason ); 345 | } 346 | } finally { 347 | con.close(); 348 | } 349 | 350 | return new HttpRedirect("resetMail"); 351 | } 352 | 353 | public User getUserById(String id, DirContext con) throws NamingException { 354 | String dn = "cn=" + id + "," + params.newUserBaseDN(); 355 | return new User(con.getAttributes(dn)); 356 | } 357 | 358 | /** 359 | * Object that represents some user in LDAP. 360 | */ 361 | public class User { 362 | /** 363 | * User ID, such as 'kohsuke' 364 | */ 365 | public final String id; 366 | /** 367 | * Email address. 368 | */ 369 | public final String mail; 370 | 371 | public User(String id, String mail) { 372 | this.id = id; 373 | this.mail = mail; 374 | } 375 | 376 | public User(Attributes att) throws NamingException { 377 | id = (String) att.get("cn").get(); 378 | mail = (String) att.get("mail").get(); 379 | } 380 | 381 | public String getDn() { 382 | return String.format("cn=%s,%s", id, params.newUserBaseDN()); 383 | } 384 | 385 | public void modifyPassword(DirContext con, String password) throws NamingException { 386 | con.modifyAttributes(getDn(),REPLACE_ATTRIBUTE,new BasicAttributes("userPassword",PasswordUtil.hashPassword(password))); 387 | LOGGER.info("User "+id+" reset the password: "+mail); 388 | } 389 | 390 | public void modifyEmail(DirContext con, String email) throws NamingException { 391 | con.modifyAttributes(getDn(),REPLACE_ATTRIBUTE,new BasicAttributes("mail",email)); 392 | LOGGER.info("User "+id+" reset the email address to: "+email); 393 | } 394 | 395 | /** 396 | * Sends a new password to this user. 397 | */ 398 | public void mailPassword(String password) throws MessagingException { 399 | mail(mail, "Your access to Jenkins resources", "Your userid is " + id + "\n" + 400 | "Your temporary password is " + password + "\n" + 401 | "\n" + 402 | "Please visit " + getUrl() + " and update your password and profile\n", "text/plain"); 403 | } 404 | 405 | /** 406 | * Sends a new password to this user. 407 | */ 408 | public void mailAccountCreated(boolean sendPassword, String password, @CheckForNull String message) throws MessagingException { 409 | mail(mail, "New account on the Jenkins project infrastructure", 410 | "Dear recipient, \n\n" + 411 | "We have created a new Jenkins project account for you. Your new user ID is " + id + "\n" + 412 | ( sendPassword ? "Your temporary password is " + password + "\n" : "" ) + 413 | ( StringUtils.isNotBlank(message) ? message + "\n" : "" ) + 414 | "\n" + 415 | ( sendPassword ? "Please visit " + getUrl() + " and update your password and profile\n" : "" ), 416 | "text/plain"); 417 | } 418 | 419 | /** 420 | * Sends a new password and a password reset notification to this user. 421 | */ 422 | public void mailPasswordReset(String password, @CheckForNull String requestedByUser, @CheckForNull String reason) throws MessagingException { 423 | mail(mail, "Password reset on the Jenkins project infrastructure", 424 | "Your Jenkins account password was reset. " + 425 | (requestedByUser != null ? String.format("It was requested by user %s. ", requestedByUser) : "") + 426 | (StringUtils.isNotBlank(reason) ? String.format("Reason: %s. ", reason) : "") + 427 | "Your userid is " + id + "\n" + 428 | "Your temporary password is " + password + "\n" + 429 | "\n" + 430 | "Please visit " + getUrl() + " and update your password and profile\n", "text/plain"); 431 | } 432 | 433 | public void delete(DirContext con) throws NamingException { 434 | con.destroySubcontext(getDn()); 435 | LOGGER.info("User " + id + " deleted"); 436 | } 437 | } 438 | 439 | private Session createJavaMailSession() { 440 | Session session; 441 | Properties props = new Properties(System.getProperties()); 442 | props.put("mail.smtp.host",params.smtpServer()); 443 | props.put("mail.smtp.port", params.smtpPort()); 444 | if(params.smtpAuth()) { 445 | props.put("mail.smtp.auth", params.smtpAuth()); 446 | props.put("mail.smtp.starttls.enable", true); 447 | session = Session.getInstance(props, 448 | new Authenticator() { 449 | protected PasswordAuthentication getPasswordAuthentication() { 450 | return new PasswordAuthentication(params.smtpUser(), params.smtpPassword()); 451 | } 452 | }); 453 | } else { 454 | session = Session.getInstance(props); 455 | } 456 | return session; 457 | } 458 | 459 | /** 460 | * Search LDAP with the given keyword, returning matching users. 461 | */ 462 | public Iterator searchByWord(String idOrMail, DirContext con) throws NamingException { 463 | final NamingEnumeration e = con.search(params.newUserBaseDN(), "(|(mail={0})(cn={0}))", new Object[]{idOrMail}, new SearchControls()); 464 | return new Iterator() { 465 | public boolean hasNext() { 466 | return e.hasMoreElements(); 467 | } 468 | 469 | public User next() { 470 | try { 471 | return new User(e.nextElement().getAttributes()); 472 | } catch (NamingException x) { 473 | throw new RuntimeException(x); 474 | } 475 | } 476 | 477 | public void remove() { 478 | throw new UnsupportedOperationException(); 479 | } 480 | }; 481 | } 482 | 483 | public LdapContext connect() throws NamingException { 484 | return connect(params.managerDN(), params.managerPassword()); 485 | } 486 | 487 | public LdapContext connect(String dn, String password) throws NamingException { 488 | Hashtable env = new Hashtable(); 489 | env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory"); 490 | env.put(Context.PROVIDER_URL, params.server()); 491 | env.put(Context.SECURITY_PRINCIPAL, dn); 492 | env.put(Context.SECURITY_CREDENTIALS, password); 493 | return new InitialLdapContext(env, null); 494 | } 495 | 496 | /** 497 | * Handles the login form submission. 498 | */ 499 | public HttpResponse doDoLogin( 500 | @QueryParameter String userid, 501 | @QueryParameter String password, 502 | @QueryParameter String from 503 | ) { 504 | login(userid, password); 505 | 506 | // to limit the redirect to this application, require that the from URL starts from '/' 507 | if (from==null || !from.startsWith("/")) from="/"; 508 | return HttpResponses.redirectTo(from); 509 | } 510 | 511 | private Myself login(String userid, String password) throws UserError { 512 | if (StringUtils.isBlank(userid)) { 513 | throw new UserError("Missing username"); 514 | } 515 | if (StringUtils.isBlank(password)) { 516 | throw new UserError("Missing password"); 517 | } 518 | 519 | String dn = "cn=" + userid + "," + params.newUserBaseDN(); 520 | try { 521 | LdapContext context = connect(dn, password); // make sure the password is valid 522 | try { 523 | Myself myself = new Myself(this, dn, context.getAttributes(dn), getGroups(dn,context)); 524 | Stapler.getCurrentRequest().getSession().setAttribute(Myself.class.getName(), myself); 525 | return myself; 526 | } finally { 527 | context.close(); 528 | } 529 | } catch (AuthenticationException e) { 530 | throw new UserError(String.format("User \"%s\" not found or password incorrect %n", userid) ); 531 | } catch (Exception e) { 532 | String errorId = String.valueOf(Math.round(Math.random() * 1e8)); 533 | LOGGER.log(Level.SEVERE, "Login error " + errorId, e); 534 | throw new UserError("Something went wrong. Please try again later.", errorId); 535 | } 536 | } 537 | 538 | /** 539 | * Obtains the group of the user specified by the given DN. 540 | */ 541 | Set getGroups(String dn, LdapContext context) throws NamingException { 542 | Set groups = new HashSet(); 543 | SearchControls c = new SearchControls(); 544 | c.setReturningAttributes(new String[]{"cn"}); 545 | c.setSearchScope(SUBTREE_SCOPE); 546 | NamingEnumeration e = context.search("dc=jenkins-ci,dc=org", "(& (objectClass=groupOfNames) (member={0}))", new Object[]{dn}, c); 547 | while (e.hasMore()) { 548 | groups.add(e.nextElement().getAttributes().get("cn").get().toString()); 549 | } 550 | return groups; 551 | } 552 | 553 | public HttpResponse doLogout(StaplerRequest req) { 554 | req.getSession().invalidate(); 555 | return HttpResponses.redirectToDot(); 556 | } 557 | 558 | public boolean isLoggedIn() { 559 | return Myself.current() !=null; 560 | } 561 | 562 | public boolean isAdmin() { 563 | Myself myself = Myself.current(); 564 | return myself !=null && myself.isAdmin(); 565 | } 566 | 567 | /** 568 | * If the user has already logged in, retrieve the current user, otherwise 569 | * send the user to the login page. 570 | */ 571 | public Myself getMyself() { 572 | Myself myself = Myself.current(); 573 | if (myself == null) { 574 | return needToLogin(); 575 | } 576 | return myself; 577 | } 578 | 579 | private Myself needToLogin() { 580 | // needs to login 581 | StaplerRequest req = Stapler.getCurrentRequest(); 582 | 583 | String authHeader = req.getHeader("Authorization"); 584 | if (authHeader != null) { // Basic auth 585 | if (!authHeader.startsWith("Basic")) { 586 | throw HttpResponses.error(403, "Only Basic authentication is supported"); 587 | } 588 | byte[] userPasswordBytes = Base64.getDecoder().decode(authHeader.trim().substring(6)); 589 | String[] res = new String(userPasswordBytes).split(":"); 590 | String user = res[0]; 591 | String password = res[1]; 592 | return login(user, password).withRESTAuthentication(true); 593 | } 594 | 595 | StringBuilder from = new StringBuilder(req.getRequestURI()); 596 | if (req.getQueryString()!=null) 597 | from.append('?').append(req.getQueryString()); 598 | try { 599 | throw HttpResponses.redirectViaContextPath("login?from="+ URLEncoder.encode(from.toString(),"UTF-8")); 600 | } catch (UnsupportedEncodingException e) { 601 | throw new AssertionError(e); 602 | } 603 | } 604 | 605 | /** 606 | * This is a test endpoint to make sure the reverse proxy forwarding is working. 607 | */ 608 | public HttpResponse doForwardTest(@Header("X-Forwarded-For") String header) { 609 | return HttpResponses.plainText(header); 610 | } 611 | 612 | public AdminUI getAdmin() { 613 | if (!getMyself().isAdmin()) { 614 | throw HttpResponses.forbidden(); 615 | } 616 | return new AdminUI(this); 617 | } 618 | 619 | private static final Logger LOGGER = Logger.getLogger(Application.class.getName()); 620 | 621 | private static final Pattern VALID_ID = Pattern.compile("[a-z0-9_]+"); 622 | 623 | public static final List EMAIL_BLACKLIST = Arrays.asList( 624 | // TODO: Integrate with https://github.com/wesbos/burner-email-providers 625 | // Also can use this to check if disposable service: http://www.block-disposable-email.com/cms/try/ 626 | "@akglsgroup.com", 627 | "@anappthat.com", 628 | "@boximail.com", 629 | "@clrmail.com", 630 | "@dodsi.com", 631 | "@eelmail.com", 632 | "@getairmail.com", 633 | "@grandmamail.com", 634 | "@grandmasmail.com", 635 | "@guerrillamail.com", 636 | "@imgof.com", 637 | "@mailcatch.com", 638 | "@maildx.com", 639 | "@mailinator.com", 640 | "@mailnesia.com", 641 | "@my10minutemail.com", 642 | "@rediffmail.com", 643 | "@sharklasers.com", 644 | "@thrma.com", 645 | "@tryalert.com", 646 | "@vomoto.com", 647 | "@webtrackker.com", 648 | "@yahoo.co.id", 649 | "@yeslifecyclemarketing.com", 650 | "@yopmail.com", 651 | "@zetmail.com", 652 | "abdhesh090@gmail.com", 653 | "abdheshnir.vipra@gmail.com", 654 | "adnanishami28", 655 | "adreahilton@gmail.com", 656 | "airtelshopstore", 657 | "ajayrudelee@gmail.com", 658 | "ajit86153@gmail.com", 659 | "ajymaansingh@gmail.com", 660 | "ak384304@gmail.com", 661 | "akleshnirala@gmail.com", 662 | "alamsarfaraz791@gmail.com", 663 | "albertthomas", 664 | "aliskimis@gmail.com", 665 | "allymptedpokdebraj@gmail.com", 666 | "amarniket4@codehot.co.uk", 667 | "ambikaku12@gmail.com", 668 | "amiteshku12@gmail.com", 669 | "andentspourita@gmail.com", 670 | "andorclifs@gmail.com", 671 | "andrewd@ghostmail.com", 672 | "andrusmith", 673 | "angthpofphilip@gmail.com", 674 | "anilkandpal0@gmail.com", 675 | "anilsingh7885945@gmail.com", 676 | "anjilojilo@gmail.com", 677 | "anshikaescorts@gmail.com", 678 | "april.caword1589@gmail.com", 679 | "apwebs7012@yahoo.com", 680 | "apwebs70@gmail.com", 681 | "arena.wilson91@gmail.com", 682 | "arth.smith@yandex.com", 683 | "asadk7856", 684 | "asadkiller2@gmail.com", 685 | "asadwoz", 686 | "ashwanikumar", 687 | "avanrajput5@gmail.com", 688 | "avanrajput5@gmail.com", 689 | "baalzebub12021996@gmail.com", 690 | "balramchoudhary.ballu@gmail.com", 691 | "baueierjose@gmail.com", 692 | "bcmdsbncskj@yandex.com", 693 | "besto.sty@yandex.com", 694 | "bidupan12@gmail.com", 695 | "bill.cardy1366@gmail.com", 696 | "billydoch021@gmail.com", 697 | "biodotlab", 698 | "boleshahuja88@gmail.com", 699 | "boymorpoupamm@gmail.com", 700 | "caulnurearkporjohnh@gmail.com", 701 | "choutpoyjenniferm@gmail.com", 702 | "ciodsjiocxjosa@yandex.com", 703 | "clarencepatterson570@gmail.com", 704 | "coatseardeaspozwilliamb", 705 | "cooktimseo@gmail.com", 706 | "cooperdavidd@gmail.com", 707 | "crsgroupindia@gmail.com", 708 | "daduajiggy@gmail.com", 709 | "dasdasdsas32@gmail.com", 710 | "davidtech46@gmail.com", 711 | "deal4udeal4me@gmail.com", 712 | "deepak19795up@gmail.com", 713 | "deepakkumar02singh@gmail.com", 714 | "dersttycert101@gmail.com", 715 | "devilr724", 716 | "doetpouernestp@gmail.com", 717 | "donallakarpissaa@gmail.com", 718 | "dr74402@gmail.com", 719 | "drruytuyj@gmail.com", 720 | "dutchess.meethi@gmail.com", 721 | "dwari.hdyr528@gmail.com", 722 | "emaxico", 723 | "estherj@ghostmail.com", 724 | "ethanluna635@gmail.com", 725 | "evagreen277@gmail.com", 726 | "fievepowgeorgeg@gmail.com", 727 | "fifixtpoqpatrickh@gmail.com", 728 | "fishepoemary@gmail.com", 729 | "flustpozwilliamp@gmail.com", 730 | "folk.zin87@gmail.com", 731 | "fragendpotmauriciok@gmail.com", 732 | "gallifingspoyjoannel@gmail.com", 733 | "gamblerbhaijaan@gmail.com", 734 | "georgegallego.com@gmail.com", 735 | "georgiaaby@gmail.com", 736 | "gladyskempf427@gmail.com", 737 | "hatteroublepocmartha@gmail.com", 738 | "hauptnuo214@gmail.com", 739 | "hcjxdbschjbdsj", 740 | "hcuiodsciodso@yandex.com", 741 | "henrymullins", 742 | "herstpopenriqued@gmail.com", 743 | "himeshsinghiq@gmail.com", 744 | "hipearspodarthurd@gmail.com", 745 | "hontpojpatricia", 746 | "hounchpowjohn@gmail.com", 747 | "howerpofharold@gmail.com", 748 | "hpprinter", 749 | "hrrbanga", 750 | "hsharish", 751 | "huin.lisko097@gmail.com", 752 | "hwayne2812", 753 | "ik96550@gmail.com", 754 | "inchestoacres@gmail.com", 755 | "intelomedia02@gmail.com", 756 | "intuitphonenumber", 757 | "iqinfotech", 758 | "iscopedigital11@gmail.com", 759 | "italygroupspecialist@gmail.com", 760 | "jacobshown1@gmail.com", 761 | "jamersonnvy309@gmail.com", 762 | "jamesdelvin2@gmail.com", 763 | "janes6521@gmail.com", 764 | "janessmith", 765 | "jayshown81@gmail.com", 766 | "jecksonmike1512@gmail.com", 767 | "jeevikasraj@gmail.com", 768 | "jhonsinha", 769 | "jiloanjilo", 770 | "jim.cook2681@gmail.com", 771 | "jksadnhk@gmail.com", 772 | "jmike7162@gmail.com", 773 | "jmtechnosolve@gmail.com", 774 | "johngarry227@gmail.com", 775 | "johnmaclan1@gmail.com", 776 | "johnmatty55@gmail.com", 777 | "johnmcclan009@gmail.com", 778 | "johnmclean278@gmail.com", 779 | "johnmikulis8", 780 | "johnnycolvin428@gmail.com", 781 | "johnprashar1@gmail.com", 782 | "johnsinha", 783 | "johnydeep0712@gmail.com", 784 | "joshuahurst928@gmail.com", 785 | "jusbabyespowbobby@gmail.com", 786 | "kalidass34212@gmail.com", 787 | "karansinghrawat15@gmail.com", 788 | "kashyap.kashyap07@yahoo.in", 789 | "kaveribhardwaj@outlook.com", 790 | "kevin24by7@gmail.com", 791 | "kisamar7@gmail.com", 792 | "kk+spamtest@kohsuke.org", 793 | "kripalsingh446@gmail.com", 794 | "krishgail30@yahoo.com", 795 | "ks0449452@gmail.com", 796 | "kukurkutta9@gmail.com", 797 | "kumar.raghavendra84@gmail.com", 798 | "kumar.uma420@gmail.com", 799 | "kumar0121@yahoo.com", 800 | "kumarprem", 801 | "kumarsujit899@gmail.com", 802 | "laknerickman@gmail.com", 803 | "laptoprepair", 804 | "larrysilva", 805 | "leonard.freddie@yandex.com", 806 | "litagray931@gmail.com", 807 | "litawilliam36@gmail.com", 808 | "loefflerpay462@gmail.com", 809 | "loksabha100@gmail.com", 810 | "lovebhaiseo@gmail.com", 811 | "lutherorea2807@gmail.com", 812 | "mac2help@outlook.com", 813 | "mac2help@outlook.com", 814 | "macden", 815 | "madeleineforsyth290@gmail.com", 816 | "maineeru@gnail.com", 817 | "makbaldwin1@gmail.com", 818 | "manasfirdose", 819 | "maohinseeeeeee@outlook.com", 820 | "mariavitory1212@gmail.com", 821 | "marklewis0903@gmail.com", 822 | "masmartin71@gmail.com", 823 | "mehrabharat137@gmail.com", 824 | "michelbavan@gmail.com", 825 | "mk2434990@gmail.com", 826 | "mmmarsh12@gmail.com", 827 | "mohandaerer", 828 | "mohankeeded", 829 | "monubhardwaj12451@gmail.com", 830 | "monurobert", 831 | "morrisonjohn293@gmail.com", 832 | "msofficeservices13@gmail.com", 833 | "mujafer@yandex.com", 834 | "nalspoibarbarab@gmail.com", 835 | "ncrpoo", 836 | "ncrraanisapna@gmail.com", 837 | "ncrradha@gmail.com", 838 | "ncrrohit", 839 | "ncrsona", 840 | "nelsonnelsen11021992@gmail.com", 841 | "newellnewallne56@gmail.com", 842 | "nextgenwebstore@gmail.com", 843 | "nirajsinghrawat12@gmail.com", 844 | "nishanoor32", 845 | "nishuyadav257@gmail.com", 846 | "obat@", 847 | "ollouretypotida@gmail.com", 848 | "outlookhelpline", 849 | "pala41667@gmail.com", 850 | "paroccepoytamarac@gmail.com", 851 | "paulseanseo91@gmail.com", 852 | "pawankundu99@gmail.com", 853 | "petersenk509@gmail.com", 854 | "petersmith2331@gmail.com", 855 | "pintu", 856 | "pk76997246@gmail.com", 857 | "pogogames483@gmail.com", 858 | "poonamkamalpatel@gmail.com", 859 | "porterquines@gmail.com", 860 | "poweltpowwilliamm@gmail.com", 861 | "pranay4job@gmail.com", 862 | "pratik.vipra@gmail.com", 863 | "premk258@gmail.com", 864 | "printerhelplinenumber@gmail.com", 865 | "priturpocdickr@gmail.com", 866 | "quickbook", 867 | "qukenservicess", 868 | "r.onysokha@gmail.com", 869 | "rahul4cool2003@gmail.com", 870 | "rahuldheere@gmail.com", 871 | "rajdsky7@gmail.com", 872 | "rajkumarskbd@gmail.com", 873 | "rajuk0838@gmail.com", 874 | "ramlakhaann@gmail.com", 875 | "randyortam68@gmail.com", 876 | "ransonfreeman@gmail.com", 877 | "rasidegingpoflaceyz@gmail.com", 878 | "ravi1991allahabaduniversity@gmail.com", 879 | "ravirknayak@gmail.com", 880 | "ravis70291@gmail.com", 881 | "rawatmeenakshi12123s", 882 | "rawatsonam", 883 | "rehel55rk@gmail.com", 884 | "righttechnical", 885 | "rikybhel23@gmail.com", 886 | "rodriquesnuv728@gmail.com", 887 | "rohitsharma7294@outlook.com", 888 | "rohitsona121090@gmail.com", 889 | "sajankaur5@gmail.com", 890 | "samit8974@gmail.com", 891 | "sandy@voip4callcenters.com", 892 | "sandysharmja121@gmail.com", 893 | "saundraclogston@gmail.com", 894 | "sawanjha17@gmail.com", 895 | "sayyedabrash110202@gmail.com", 896 | "sbabita149@gmail.com", 897 | "sellikepovscotta@gmail.com", 898 | "seo@gmail.com", 899 | "seosupport", 900 | "seoxpertchandan@gmail.com", 901 | "service.thepc@yandex.com", 902 | "shashisemraha@gmail.com", 903 | "shichpodlakeisha@gmail.com", 904 | "shilpikispotta2508@gmail.com", 905 | "shobha.kachhap23@gmail.com", 906 | "shyamtengo91@gmail.com", 907 | "simon.ken7@gmail.com", 908 | "singhavan31@gmail.com", 909 | "singhmukul", 910 | "skprajapaty@gmail.com", 911 | "sman33935@gmail.com", 912 | "smartsolution3000@gmail.com", 913 | "smithlora912@gmail.com", 914 | "smithmartin919@gmail.com", 915 | "smithmaxseo@gmail.com", 916 | "sneharajput931@gmail.com", 917 | "snjbisth8@gmail.com", 918 | "sonu70251", 919 | "sorianonvy291@gmail.com", 920 | "spyindia", 921 | "spyvikash", 922 | "stephanflorian1@gmail.com", 923 | "stuartbinny12021996@gmail.com", 924 | "sty.besto", 925 | "stybesto13", 926 | "sujatakumari8236@gmail.com", 927 | "sujitkumarthakur8@gmail.com", 928 | "sundeepsa123@gmail.com", 929 | "sunflowerrosy@outlook.com", 930 | "sunilkundujat@gmail.com", 931 | "sunjara10@gmail.com", 932 | "sweenypar210@gmail.com", 933 | "telemarket3004", 934 | "teresarothschild198@gmail.com", 935 | "therewpoxjamix", 936 | "thjyt@yandex.com", 937 | "thornestedpokrodneyn@gmail.com", 938 | "tinku8384@gmail.com", 939 | "tomcopper6@gmail.com", 940 | "tomhanks1121@gmail.com", 941 | "toren55rk@gmail.com", 942 | "uma.shank122451ddsds@gmail.com", 943 | "updateursoftware2016@gmail.com", 944 | "useenpofchristopherc@gmail.com", 945 | "vcb@gmail.com", 946 | "viz.michel@gmail.com", 947 | "vr4vikasrastogi@gmail.com", 948 | "vvicky4001@gmail.com", 949 | "vvicky4003@gmail.com", 950 | "watpad", 951 | "webdevelopera@gmail.com", 952 | "webtracker", 953 | "webtrackker", 954 | "win.tech", 955 | "winstond@tutanota.com", 956 | "wittepepobjustina@gmail.com", 957 | "yadavqs@gmail.com", 958 | "yadavshalini501@gmail.com", 959 | "yashraj_one", 960 | "yetwallpoudonnal@gmail.com", 961 | "youltaspocdonald@gmail.com", 962 | "youmint.lav@gmail.com", 963 | "ytdeqwduqwy@yandex.com", 964 | "zebakhan.ssit@gmail.com", 965 | "zozojams11@gmail.com" 966 | ); 967 | 968 | public static final List IP_BLACKLIST = Arrays.asList( 969 | "1.186.172.", 970 | "1.187.114.172", 971 | "1.187.118.175", 972 | "1.187.123.123", 973 | "1.187.126.76", 974 | "1.187.162.39", 975 | "1.22.130.142", 976 | "1.22.131.140", 977 | "1.22.164.211", 978 | "1.22.164.227", 979 | "1.22.212.209", 980 | "1.22.38.186", 981 | "1.22.39.244", 982 | "1.23.110.86", 983 | "1.39.101.93", 984 | "1.39.32.", 985 | "1.39.33.", 986 | "1.39.34.", 987 | "1.39.35.", 988 | "1.39.40.", 989 | "1.39.50.144", 990 | "1.39.51.63", 991 | "101.212.67.25", 992 | "101.212.69.213", 993 | "101.212.71.120", 994 | "101.222.175.13", 995 | "101.56.169.183", 996 | "101.56.181.69", 997 | "101.56.2.232", 998 | "101.57.173.94", 999 | "101.57.198.190", 1000 | "101.57.198.201", 1001 | "101.57.198.22", 1002 | "101.57.7.41", 1003 | "101.59.227.148", 1004 | "101.59.72.90", 1005 | "101.59.76.223", 1006 | "101.60.", 1007 | "101.63.200.188", 1008 | "103.10.197.194", 1009 | "103.18.72.91", 1010 | "103.19.153.130", 1011 | "103.192.64.", 1012 | "103.192.65.", 1013 | "103.192.66.163", 1014 | "103.204.168.18", 1015 | "103.22.172.142", 1016 | "103.226.202.171", 1017 | "103.226.202.211", 1018 | "103.233.116.124", 1019 | "103.233.118.222", 1020 | "103.233.118.230", 1021 | "103.245.118.", 1022 | "103.251.61.50", 1023 | "103.254.154.229", 1024 | "103.43.33.101", 1025 | "103.43.35.42", 1026 | "103.44.18.221", 1027 | "103.47.168.129", 1028 | "103.47.168.57", 1029 | "103.47.169.137", 1030 | "103.47.169.7", 1031 | "103.49.49.49", 1032 | "103.50.151.68", 1033 | "103.55.", 1034 | "104.128.21.186", // burleigh 1035 | "104.156.228.84", // http://www.ipvoid.com/scan/104.156.228.84 1036 | "104.156.240.140", // http://www.ipvoid.com/scan/104.156.240.140 1037 | "104.200.154.4", // http://www.ipvoid.com/scan/104.200.154.4 1038 | "104.224.35.122", // http://www.ipvoid.com/scan/104.224.35.122/ 1039 | "104.236.123.17", // persistent spammer 1040 | "104.236.3.101", // casejackson49 1041 | "104.238.169.58", // http://www.ipvoid.com/scan/104.238.169.58 1042 | "106.201.144.243", 1043 | "106.201.174.113", 1044 | "106.201.196.70", 1045 | "106.201.33.175", 1046 | "106.202.84.222", 1047 | "106.204.124.188", 1048 | "106.204.142.176", 1049 | "106.204.194.124", 1050 | "106.204.236.224", 1051 | "106.204.246.196", 1052 | "106.204.46.93", 1053 | "106.204.50.214", 1054 | "106.205.188.247", 1055 | "106.205.56.253", 1056 | "106.208.183.157", 1057 | "106.215.176.34", 1058 | "106.67.102.143", 1059 | "106.67.113.167", 1060 | "106.67.118.250", 1061 | "106.67.28.163", 1062 | "106.67.28.22", 1063 | "106.67.46.209", 1064 | "106.67.76.237", 1065 | "106.67.89.196", 1066 | "106.76.167.41", 1067 | "106.79.57.84", 1068 | "106.79.59.178", 1069 | "107.168.18.212", // mujafer_l 1070 | "107.191.46.37", // williamthousus35 1071 | "107.191.53.51", // persistent spammer 1072 | "108.59.10.141", // http://www.ipvoid.com/scan/108.59.10.141 1073 | "108.61.184.146", // adnanishami28 1074 | "109.163.234.8", // http://www.ipvoid.com/scan/109.163.234.8 1075 | "109.201.137.46", // tomhanks1121 1076 | "110.172.140.98", 1077 | "110.227.181.55", 1078 | "110.227.183.246", 1079 | "110.227.183.36", 1080 | "111.125.137.83", 1081 | "111.93.63.62", 1082 | "112.110.118.56", 1083 | "112.110.2.245", 1084 | "112.110.21.8", 1085 | "112.110.22.229", 1086 | "112.196.147.", 1087 | "112.196.160.", 1088 | "112.196.170.150", 1089 | "112.196.170.8", 1090 | "114.143.173.139", 1091 | "115.111.183.14", 1092 | "115.111.223.43", 1093 | "115.112.159.250", 1094 | "115.114.191.92", 1095 | "115.115.131.166", 1096 | "115.160.250.34", 1097 | "115.184.", 1098 | "115.249.46.242", 1099 | "116.202.32.38", 1100 | "116.202.36.", 1101 | "116.203.", 1102 | "117.193.130.11", 1103 | "117.198.", 1104 | "117.201.159.73", 1105 | "117.201.166.21", 1106 | "117.242.5.201", 1107 | "119.81.230.137", 1108 | "119.81.249.132", // http://www.ipvoid.com/scan/119.81.249.132 1109 | "119.81.253.243", // http://www.ipvoid.com/scan/119.81.253.243 1110 | "119.82.95.142", 1111 | "120.57.17.65", 1112 | "120.57.86.248", 1113 | "120.59.205.205", 1114 | "121.241.96.5", 1115 | "121.242.40.15", 1116 | "121.242.77.200", 1117 | "121.244.181.162", 1118 | "121.244.95.1", 1119 | "121.245.126.7", 1120 | "121.245.137.28", 1121 | "122.162.88.67", 1122 | "122.167.104.9", 1123 | "122.169.130.19", 1124 | "122.172.222.232", 1125 | "122.173.", 1126 | "122.175.221.219", 1127 | "122.176.18.41", 1128 | "122.176.78.14", 1129 | "122.177.", 1130 | "122.180.", 1131 | "123.136.209.119", 1132 | "123.239.77.189", 1133 | "123.254.107.229", 1134 | "124.41.241.203", 1135 | "124.42.12.77", 1136 | "125.16.2.102", 1137 | "125.63.101.236", 1138 | "125.63.104.53", 1139 | "125.63.107.141", 1140 | "125.63.107.204", 1141 | "125.63.73.249", 1142 | "125.63.96.184", 1143 | "125.63.99.102", 1144 | "128.199.242.223", // persistent spammer 1145 | "136.185.192.239", 1146 | "138.128.180.", 1147 | "14.141.1.58", 1148 | "14.141.148.206", 1149 | "14.141.148.222", 1150 | "14.141.163.58", 1151 | "14.141.49.210", 1152 | "14.141.51.5", 1153 | "14.96.", 1154 | "14.98.", 1155 | "154.127.63.33", // saundraclogston 1156 | "155.254.246.", 1157 | "155.94.183.102", // grantp 1158 | "158.255.208.72", 1159 | "162.245.144.142", // jamesdevlin 1160 | "167.114.159.186", // Persistent spammer 1161 | "169.57.0.235", // http://www.ipvoid.com/scan/169.57.0.235 1162 | "171.48.32.3", 1163 | "171.48.38.188", 1164 | "171.50.131.221", 1165 | "171.50.146.100", 1166 | "171.50.42.142", 1167 | "172.98.67.25", // http://www.ipvoid.com/scan/172.98.67.25 1168 | "172.98.67.71", // http://www.ipvoid.com/scan/172.98.67.71 1169 | "174.140.170.121", // monurobert 1170 | "176.222.33.42", // imistupidtammy 1171 | "176.53.21.213", // http://www.ipvoid.com/scan/176.53.21.213 1172 | "176.67.85.1", // http://www.ipvoid.com/scan/176.67.85.1 1173 | "176.67.86.", // laptoprepair gmail guy and Persistent spammers 1174 | "176.67.86.36", // Persistent spammer 1175 | "177.154.139.203", // http://www.ipvoid.com/scan/177.154.139.203 1176 | "180.151.228.235", // https://github.com/jenkins-infra/account-app/commit/a04efa7690fa6fc228a407d6707cfe24bf1dd995 1177 | "180.151.246.3", // https://github.com/jenkins-infra/account-app/commit/a04efa7690fa6fc228a407d6707cfe24bf1dd995 1178 | "180.151.30.243", // https://github.com/jenkins-infra/account-app/commit/a04efa7690fa6fc228a407d6707cfe24bf1dd995 1179 | "180.151.7.42", // https://github.com/jenkins-infra/account-app/commit/a04efa7690fa6fc228a407d6707cfe24bf1dd995 1180 | "180.151.84.234", // https://github.com/jenkins-infra/account-app/commit/a04efa7690fa6fc228a407d6707cfe24bf1dd995 1181 | "180.254.96.177", // http://www.ipvoid.com/scan/180.254.96.177 1182 | "181.41.197.63", // Persistent spammer 1183 | "182.156.72.162", 1184 | "182.156.89.34", 1185 | "182.64.", 1186 | "182.65.32.63", 1187 | "182.68.", 1188 | "182.69.", 1189 | "182.71.71.102", 1190 | "182.73.117.142", 1191 | "182.73.182.170", 1192 | "182.74.135.26", 1193 | "182.74.88.42", 1194 | "182.75.144.58", 1195 | "182.75.176.202", 1196 | "182.77.14.159", 1197 | "182.77.4.162", 1198 | "182.77.8.24", 1199 | "182.77.8.92", 1200 | "183.82.199.55", 1201 | "185.108.128.12", // http://www.ipvoid.com/scan/185.108.128.12 1202 | "185.22.232.91", // Persistent spammer 1203 | "185.61.148.226", // Persistent spammer 1204 | "188.116.36.188", // Persistent spammer 1205 | "188.172.220.71", // johnmclean007 1206 | "192.171.254.207", // andrewd 1207 | "192.200.23.16", // anital 1208 | "192.230.49.120", // winstond 1209 | "193.182.144.106", // Persistent spammer 1210 | "196.207.106.219", 1211 | "196.207.107.56", 1212 | "198.203.29.123", // winstond 1213 | "198.8.80.172", // http://www.ipvoid.com/scan/198.8.80.172 1214 | "200.80.48.34", // babhy 1215 | "202.122.134.254", 1216 | "202.159.213.10", 1217 | "202.3.120.6", 1218 | "202.53.94.4", 1219 | "202.91.134.66", 1220 | "202.91.134.67", 1221 | "202.91.76.164", 1222 | "202.91.76.82", 1223 | "203.110.83.66", 1224 | "203.122.16.168", 1225 | "203.122.41.130", 1226 | "203.122.7.236", 1227 | "203.99.192.210", 1228 | "211.79.127.11", // Persistent spammer 1229 | "212.7.220.136", // aprilcowardy 1230 | "212.83.165.204", // http://www.ipvoid.com/scan/212.83.165.204/ 1231 | "213.183.56.51", // Persistent spammer 1232 | "216.185.103.139", // http://www.ipvoid.com/scan/216.185.103.139 1233 | "217.114.211.246", // http://www.ipvoid.com/scan/106.51.27.106 1234 | "223.176.141.173", 1235 | "223.176.142.230", 1236 | "223.176.143.154", 1237 | "223.176.152.27", 1238 | "223.176.156.193", 1239 | "223.176.159.235", 1240 | "223.176.165.11", 1241 | "223.176.170.100", 1242 | "223.176.176.254", 1243 | "223.176.177.174", 1244 | "223.176.178.24", 1245 | "223.176.188.147", 1246 | "223.176.189.144", 1247 | "223.176.190.59", 1248 | "223.180.245.176", 1249 | "223.183.100.179", 1250 | "223.183.67.247", 1251 | "223.225.42.57", 1252 | "223.229.99.61", 1253 | "23.81.47.235", // leonard_freddie 1254 | "27.255.252.28", 1255 | "27.4.184.217", 1256 | "27.56.47.65", 1257 | "27.60.131.203", 1258 | "27.7.210.21", 1259 | "27.7.213.175", 1260 | "27.7.231.16", 1261 | "37.235.53.75", // Persistent spammer IP 1262 | "38.132.106.132", // http://www.ipvoid.com/scan/38.132.106.132 1263 | "38.95.108.245", // http://www.ipvoid.com/scan/38.95.108.245 1264 | "38.95.109.37", // http://www.ipvoid.com/scan/38.95.109.37 1265 | "38.95.109.67", // http://www.ipvoid.com/scan/38.95.109.67 1266 | "41.215.241.114", // Persistent spammer IP 1267 | "43.225.195.92", 1268 | "43.230.198.", 1269 | "43.239.204.41", 1270 | "43.239.68.", 1271 | "43.242.36.56", 1272 | "43.245.138.109", 1273 | "43.245.139.203", 1274 | "43.245.149.107", 1275 | "43.245.149.205", 1276 | "43.245.151.", 1277 | "43.245.156.239", 1278 | "43.245.156.94", 1279 | "43.245.158.172", 1280 | "43.245.158.5", 1281 | "43.245.209.170", 1282 | "43.245.211.", 1283 | "43.249.38.69", // Persistent spammer IP 1284 | "43.251.84.", 1285 | "43.252.24.155", 1286 | "43.252.25.153", 1287 | "43.252.25.45", 1288 | "43.252.27.35", 1289 | "43.252.27.52", 1290 | "43.252.29.202", 1291 | "43.252.29.206", 1292 | "43.252.30.75", 1293 | "43.252.30.93", 1294 | "43.252.31.133", 1295 | "43.252.31.138", 1296 | "43.252.32.181", 1297 | "43.252.33.2", 1298 | "43.252.33.70", 1299 | "43.252.34.9", 1300 | "43.252.35.80", 1301 | "45.114.63.184", 1302 | "45.115.", 1303 | "45.120.162.172", 1304 | "45.120.56.65", 1305 | "45.120.59.232", 1306 | "45.120.59.9", 1307 | "45.121.188.46", 1308 | "45.121.189.236", 1309 | "45.121.191.78", 1310 | "45.122.120.178", 1311 | "45.122.123.47", 1312 | "45.122.123.81", 1313 | "45.127.40.", 1314 | "45.127.42.219", 1315 | "45.127.42.63", 1316 | "45.127.43.154", 1317 | "45.32.226.36", // daduajiggy 1318 | "45.42.150.85", 1319 | "45.42.243.83", 1320 | "45.55.3.174", 1321 | "45.56.154.150", 1322 | "45.59.185.104", // alishalal 1323 | "46.101.245.193", // Persistent spammers 1324 | "46.165.208.207", // http://www.ipvoid.com/scan/46.165.208.207 1325 | "46.166.179.59", // Persistent spammers 1326 | "46.21.149.10", // jamesthomas (manasfirdose@gmail.com) 1327 | "46.218.58.138", // http://www.ipvoid.com/scan/46.218.58.138/ 1328 | "49.14.126.20", 1329 | "49.15.149.23", 1330 | "49.15.150.57", 1331 | "49.15.158.7", 1332 | "49.156.150.242", 1333 | "49.204.252.214", 1334 | "49.207.188.57", 1335 | "49.244.214.39", 1336 | "49.248.212.18", 1337 | "49.40.2.224", 1338 | "5.62.21.", // Anonymous proxy 1339 | "5.62.5.71", // http://www.ipvoid.com/scan/5.62.5.71 1340 | "50.31.252.31", // Persistent spammers 1341 | "54.193.91.83", // guru123qbji 1342 | "54.84.8.145", // Persistent spammers 1343 | "59.180.132.51", 1344 | "59.180.155.34", 1345 | "59.180.25.215", 1346 | "59.180.27.191", 1347 | "59.91.216.88", 1348 | "59.92.74.170", 1349 | "61.0.85.206", 1350 | "61.12.36.234", 1351 | "61.12.72.244", 1352 | "61.12.72.246", 1353 | "62.210.139.80", // proxy? twice an Indian spammer jumped to this IP 1354 | "69.28.83.89", // Persistent spammers 1355 | "69.31.101.3", // Persistent spammers 1356 | "69.65.43.205", // http://www.ipvoid.com/scan/69.65.43.205 1357 | "74.120.223.151", // petersenk509 1358 | "78.110.165.187", // robin24by7 1359 | "81.218.235.170", // http://www.ipvoid.com/scan/81.218.235.170 1360 | "91.121.219.236", // http://www.ipvoid.com/scan/91.121.219.236 1361 | "91.205.175.34", // http://www.ipvoid.com/scan/91.205.175.34 1362 | "92.114.94.106", // http://www.ipvoid.com/scan/92.114.94.106/ 1363 | "93.114.44.187", // http://www.ipvoid.com/scan/93.114.44.187 1364 | "93.115.92.169", // http://www.ipvoid.com/scan/93.115.92.169 1365 | "95.141.31.7", // http://www.ipvoid.com/scan/95.141.31.7 1366 | "95.211.101.216", // makbaldwin1 1367 | "95.211.171.164" // http://www.ipvoid.com/scan/95.211.171.164 1368 | ); 1369 | 1370 | public static final List USERID_BLACKLIST = Arrays.asList( 1371 | "babhy", 1372 | "intuitphonenumber", 1373 | "jamesbond", 1374 | "larrysilva", 1375 | "macden", 1376 | "quickbook", 1377 | "smartjane", 1378 | "turbotax", 1379 | "watpad" 1380 | ); 1381 | 1382 | public static final List NAUGHTY_BLACKLIST = Arrays.asList( 1383 | "asshole", 1384 | "bastard", 1385 | "bitches", 1386 | "dicksuck", 1387 | "fuck", 1388 | "pussy", 1389 | "vagina" 1390 | ); 1391 | 1392 | public static final String SPAM_MESSAGE = "The anti-spam system was triggered: we need your help to prove that you are human and not a robot (or a human spammer). " + 1393 | "Open an issue on the Jenkins Infrastructure Help Desk and provide the email and the userid you tried to register with, as well as the version of Jenkins you currently are running. " + 1394 | "Try to make your issue look legitimate and not like it's written by someone who doesn't know anything or just read the jenkins.io home page. " + 1395 | "If we don't think your signup request is legitimate, it won't be approved. We're a popular target of human spammers, i.e. actual people with actual web browsers trying to post their spam in our applications. "; 1396 | 1397 | // Somewhat cryptic name for cookie, so prying eyes don't know its use. 1398 | public static final String ALREADY_SIGNED_UP = "JENKINSACCOUNT"; 1399 | 1400 | /** 1401 | * Returns true if the provided url is the active url 1402 | */ 1403 | public boolean isActive(String url) { 1404 | StaplerRequest req = Stapler.getCurrentRequest(); 1405 | String currentUrl = StringUtils.strip(req.getRequestURI(), "/"); 1406 | return currentUrl.equals(url); 1407 | } 1408 | } 1409 | --------------------------------------------------------------------------------