diff --git a/pom.xml b/pom.xml index 795f312bb..e7330c07e 100644 --- a/pom.xml +++ b/pom.xml @@ -68,7 +68,7 @@ 999999-SNAPSHOT jenkinsci/${project.artifactId}-plugin - 2.504 + 2.516 ${jenkins.baseline}.3 false 1372 diff --git a/src/main/java/com/cloudbees/plugins/credentials/CredentialsDescriptor.java b/src/main/java/com/cloudbees/plugins/credentials/CredentialsDescriptor.java index 78af69ea7..146212b4f 100644 --- a/src/main/java/com/cloudbees/plugins/credentials/CredentialsDescriptor.java +++ b/src/main/java/com/cloudbees/plugins/credentials/CredentialsDescriptor.java @@ -605,4 +605,10 @@ public String toCheckUrl() { } } + /** + * @return the description of this credential descriptor. + */ + public String getDescription() { + return null; + } } diff --git a/src/main/java/com/cloudbees/plugins/credentials/CredentialsSelectHelper.java b/src/main/java/com/cloudbees/plugins/credentials/CredentialsSelectHelper.java index bfde5fe8f..640dce424 100644 --- a/src/main/java/com/cloudbees/plugins/credentials/CredentialsSelectHelper.java +++ b/src/main/java/com/cloudbees/plugins/credentials/CredentialsSelectHelper.java @@ -43,13 +43,17 @@ import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Objects; import java.util.Set; import java.util.TreeMap; import java.util.logging.Level; import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.StreamSupport; import jakarta.servlet.ServletException; import jenkins.model.Jenkins; @@ -121,6 +125,26 @@ public ModelObject resolveContext(Object context) { return context instanceof ModelObject mo ? mo : CredentialsDescriptor.findContextInPath(ModelObject.class); } + /** + * @return modifiable store actions for the context provided. + */ + @Restricted(NoExternalUse.class) + public Map> getModifiableStoreActions(ModelObject context) { + return StreamSupport.stream(CredentialsProvider.lookupStores(context).spliterator(), false) + .filter(s -> s.hasPermission(CredentialsProvider.CREATE)) + .map(CredentialsStore::getStoreAction) + .filter(Objects::nonNull) + .collect(Collectors.toMap( + CredentialsStoreAction::getDisplayName, + store -> new ArrayList<>(store.getDomains().values()), + (left, right) -> { + left.addAll(right); + return left; + }, + LinkedHashMap::new + )); + } + /** * Returns the {@link StoreItem} instances for the current Stapler request. * diff --git a/src/main/java/com/cloudbees/plugins/credentials/CredentialsStoreAction.java b/src/main/java/com/cloudbees/plugins/credentials/CredentialsStoreAction.java index 36322e573..2fcb6beeb 100644 --- a/src/main/java/com/cloudbees/plugins/credentials/CredentialsStoreAction.java +++ b/src/main/java/com/cloudbees/plugins/credentials/CredentialsStoreAction.java @@ -730,9 +730,12 @@ public CredentialsWrapper getCredential(String id) { public HttpResponse doCreateCredentials(StaplerRequest2 req) throws ServletException, IOException { getStore().checkPermission(CREATE); String requestContentType = req.getContentType(); + + String acceptHeader = req.getHeader("Accept"); if (requestContentType == null) { throw new Failure("No Content-Type header set"); } + boolean jsonResponse = acceptHeader != null && acceptHeader.contains("application/json"); if (requestContentType.startsWith("application/xml") || requestContentType.startsWith("text/xml")) { final StringWriter out = new StringWriter(); @@ -753,7 +756,20 @@ public HttpResponse doCreateCredentials(StaplerRequest2 req) throws ServletExcep } else { JSONObject data = req.getSubmittedForm(); Credentials credentials = Descriptor.bindJSON(req, Credentials.class, data.getJSONObject("credentials")); - getStore().addCredentials(domain, credentials); + boolean credentialsWereAdded = getStore().addCredentials(domain, credentials); + + if (jsonResponse) { + if (credentialsWereAdded) { + return HttpResponses.okJSON(new JSONObject() + .element("message", "Credentials created") + .element("notificationType", "SUCCESS")); + } else { + return HttpResponses.okJSON(new JSONObject() + .element("message", "Credentials with specified ID already exist") + // TODO: or domain does not exist at all? + .element("notificationType", "ERROR")); + } + } return HttpResponses.redirectTo("../../domain/" + getUrlName()); } } diff --git a/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java b/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java index 2ac56a4c1..21ac72ae9 100644 --- a/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java +++ b/src/main/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImpl.java @@ -232,6 +232,12 @@ public String getDisplayName() { return Messages.CertificateCredentialsImpl_DisplayName(); } + @Override + public String getDescription() { + // TODO - this is ChatGPT content + return "Upload a file and store it securely (for example, a key file, config file, or license file)."; + } + /** * {@inheritDoc} */ diff --git a/src/main/java/com/cloudbees/plugins/credentials/impl/UsernamePasswordCredentialsImpl.java b/src/main/java/com/cloudbees/plugins/credentials/impl/UsernamePasswordCredentialsImpl.java index c55f2dae1..19c70b59d 100644 --- a/src/main/java/com/cloudbees/plugins/credentials/impl/UsernamePasswordCredentialsImpl.java +++ b/src/main/java/com/cloudbees/plugins/credentials/impl/UsernamePasswordCredentialsImpl.java @@ -134,6 +134,12 @@ public String getDisplayName() { return Messages.UsernamePasswordCredentialsImpl_DisplayName(); } + @Override + public String getDescription() { + // TODO - this is ChatGPT content + return "Use for basic auth (username and password) to services like Git, APIs, or registries."; + } + /** * {@inheritDoc} */ diff --git a/src/main/resources/com/cloudbees/plugins/credentials/CredentialsSelectHelper/WrappedCredentialsStore/credential.jelly b/src/main/resources/com/cloudbees/plugins/credentials/CredentialsSelectHelper/WrappedCredentialsStore/credential.jelly deleted file mode 100644 index 684f1a5f5..000000000 --- a/src/main/resources/com/cloudbees/plugins/credentials/CredentialsSelectHelper/WrappedCredentialsStore/credential.jelly +++ /dev/null @@ -1,47 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/main/resources/com/cloudbees/plugins/credentials/CredentialsSelectHelper/WrappedCredentialsStore/credential_fr.properties b/src/main/resources/com/cloudbees/plugins/credentials/CredentialsSelectHelper/WrappedCredentialsStore/credential_fr.properties deleted file mode 100644 index 1a109794a..000000000 --- a/src/main/resources/com/cloudbees/plugins/credentials/CredentialsSelectHelper/WrappedCredentialsStore/credential_fr.properties +++ /dev/null @@ -1,23 +0,0 @@ -# The MIT License -# -# Copyright (c) 2014, Damien Finck -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -Kind=Type diff --git a/src/main/resources/com/cloudbees/plugins/credentials/CredentialsSelectHelper/WrappedCredentialsStore/credential_it.properties b/src/main/resources/com/cloudbees/plugins/credentials/CredentialsSelectHelper/WrappedCredentialsStore/credential_it.properties deleted file mode 100644 index 8dcb2e1c7..000000000 --- a/src/main/resources/com/cloudbees/plugins/credentials/CredentialsSelectHelper/WrappedCredentialsStore/credential_it.properties +++ /dev/null @@ -1,23 +0,0 @@ -# The MIT License -# -# Copyright (c) 2020 Alessandro Menti -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -Kind=Tipo diff --git a/src/main/resources/com/cloudbees/plugins/credentials/CredentialsSelectHelper/WrappedCredentialsStore/credential_ja.properties b/src/main/resources/com/cloudbees/plugins/credentials/CredentialsSelectHelper/WrappedCredentialsStore/credential_ja.properties deleted file mode 100644 index 532648143..000000000 --- a/src/main/resources/com/cloudbees/plugins/credentials/CredentialsSelectHelper/WrappedCredentialsStore/credential_ja.properties +++ /dev/null @@ -1,23 +0,0 @@ -# The MIT License -# -# Copyright 2015 Seiji Sogabe -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -Kind=\u7a2e\u985e diff --git a/src/main/resources/com/cloudbees/plugins/credentials/CredentialsSelectHelper/WrappedCredentialsStore/dialog_fr.properties b/src/main/resources/com/cloudbees/plugins/credentials/CredentialsSelectHelper/WrappedCredentialsStore/dialog_fr.properties deleted file mode 100644 index 49de2fda9..000000000 --- a/src/main/resources/com/cloudbees/plugins/credentials/CredentialsSelectHelper/WrappedCredentialsStore/dialog_fr.properties +++ /dev/null @@ -1,25 +0,0 @@ -# The MIT License -# -# Copyright (c) 2014, Damien Finck -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -Add\ Credentials=Ajouter des identifiants -Cancel=Annuler -Add=Ajouter diff --git a/src/main/resources/com/cloudbees/plugins/credentials/CredentialsSelectHelper/WrappedCredentialsStore/dialog_it.properties b/src/main/resources/com/cloudbees/plugins/credentials/CredentialsSelectHelper/WrappedCredentialsStore/dialog_it.properties deleted file mode 100644 index 47d047510..000000000 --- a/src/main/resources/com/cloudbees/plugins/credentials/CredentialsSelectHelper/WrappedCredentialsStore/dialog_it.properties +++ /dev/null @@ -1,26 +0,0 @@ -# The MIT License -# -# Copyright ฉ 2020 Alessandro Menti -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -Add=Aggiungi -Add\ Credentials=Aggiungi credenziali -Cancel=Annulla -Domain=Dominio diff --git a/src/main/resources/com/cloudbees/plugins/credentials/CredentialsSelectHelper/WrappedCredentialsStore/dialog_ja.properties b/src/main/resources/com/cloudbees/plugins/credentials/CredentialsSelectHelper/WrappedCredentialsStore/dialog_ja.properties deleted file mode 100644 index c28ff8473..000000000 --- a/src/main/resources/com/cloudbees/plugins/credentials/CredentialsSelectHelper/WrappedCredentialsStore/dialog_ja.properties +++ /dev/null @@ -1,25 +0,0 @@ -# The MIT License -# -# Copyright 2015 Seiji Sogabe -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -Add\ Credentials=\u8a8d\u8a3c\u60c5\u5831\u306e\u8ffd\u52a0 -Add=\u8ffd\u52a0 -Cancel=\u4e2d\u6b62 diff --git a/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/DomainWrapper/dialog.jelly b/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/DomainWrapper/dialog.jelly new file mode 100644 index 000000000..05762cc3d --- /dev/null +++ b/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/DomainWrapper/dialog.jelly @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + +
+
+
+ ${%Select a type of credential} +
+ +
+ +
+
+
+
+
+ + + + +
+
+
diff --git a/src/main/resources/com/cloudbees/plugins/credentials/CredentialsSelectHelper/WrappedCredentialsStore/dialog.jelly b/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/DomainWrapper/dialog2.jelly similarity index 55% rename from src/main/resources/com/cloudbees/plugins/credentials/CredentialsSelectHelper/WrappedCredentialsStore/dialog.jelly rename to src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/DomainWrapper/dialog2.jelly index b7cc577f4..9b97938de 100644 --- a/src/main/resources/com/cloudbees/plugins/credentials/CredentialsSelectHelper/WrappedCredentialsStore/dialog.jelly +++ b/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/DomainWrapper/dialog2.jelly @@ -24,24 +24,26 @@ --> - -
-
-

${%Add Credentials}

- -
- -
-
- - - - -
-
-
+ + + + + + +
+
+ + + + + +
+
+ + + + +
+
diff --git a/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/DomainWrapper/index.jelly b/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/DomainWrapper/index.jelly index cb2d6f832..8c6bc0d5a 100644 --- a/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/DomainWrapper/index.jelly +++ b/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/DomainWrapper/index.jelly @@ -27,16 +27,24 @@ xmlns:f="/lib/form" xmlns:c="/lib/credentials" xmlns:t="/lib/hudson"> + + + + + + - - - ${%Add Credentials} - + @@ -73,7 +81,7 @@ - ${%noCredentialsCallToAction} + diff --git a/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/DomainWrapper/newCredentials.jelly b/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/DomainWrapper/newCredentials.jelly deleted file mode 100644 index 254c7dbee..000000000 --- a/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/DomainWrapper/newCredentials.jelly +++ /dev/null @@ -1,77 +0,0 @@ - - - - - - -

${%New credentials}

- - - - - - - - ${descriptors[0].displayName} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
diff --git a/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/DomainWrapper/newCredentials_de.properties b/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/DomainWrapper/newCredentials_de.properties deleted file mode 100644 index f42d1b16c..000000000 --- a/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/DomainWrapper/newCredentials_de.properties +++ /dev/null @@ -1,25 +0,0 @@ -# -# The MIT License -# -# Copyright (c) 2013, CloudBees, Inc., Harald Albers. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -New\ Credentials=Neue Zugangsdaten -Kind=Art diff --git a/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/DomainWrapper/newCredentials_fr.properties b/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/DomainWrapper/newCredentials_fr.properties deleted file mode 100644 index 252468d14..000000000 --- a/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/DomainWrapper/newCredentials_fr.properties +++ /dev/null @@ -1,25 +0,0 @@ -# The MIT License -# -# Copyright (c) 2014, Damien Finck -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -New\ Credentials=Nouveaux identifiants -Kind=Type -OK=OK diff --git a/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/DomainWrapper/newCredentials_it.properties b/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/DomainWrapper/newCredentials_it.properties deleted file mode 100644 index 42a208eaa..000000000 --- a/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/DomainWrapper/newCredentials_it.properties +++ /dev/null @@ -1,25 +0,0 @@ -# The MIT License -# -# Copyright ฉ 2020 Alessandro Menti -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -Kind=Tipo -New\ Credentials=Nuove credenziali -OK=OK diff --git a/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/DomainWrapper/newCredentials_ja.properties b/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/DomainWrapper/newCredentials_ja.properties deleted file mode 100644 index 87997ebd9..000000000 --- a/src/main/resources/com/cloudbees/plugins/credentials/CredentialsStoreAction/DomainWrapper/newCredentials_ja.properties +++ /dev/null @@ -1,25 +0,0 @@ -# The MIT License -# -# Copyright (c) 2013 Seiji SOgabe -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. - -New\ Credentials=\u65b0\u898f\u306e\u8a8d\u8a3c\u60c5\u5831 -Kind=\u7a2e\u985e -OK=\u4fdd\u5b58 diff --git a/src/main/resources/com/cloudbees/plugins/credentials/Messages.properties b/src/main/resources/com/cloudbees/plugins/credentials/Messages.properties index 60bb67be7..5cb72a347 100644 --- a/src/main/resources/com/cloudbees/plugins/credentials/Messages.properties +++ b/src/main/resources/com/cloudbees/plugins/credentials/Messages.properties @@ -54,9 +54,8 @@ CredentialsScope.UserDisplayName=User CredentialsScope.GlobalDisplayName=Global (Jenkins, nodes, items, all child items, etc) CredentialsScope.SystemDisplayName=System (Jenkins and nodes only) CredentialsStoreAction.DisplayName=Credentials -CredentialsStoreAction.GlobalDomainDisplayName=Global credentials (unrestricted) -CredentialsStoreAction.GlobalDomainDescription=Credentials that should be available irrespective of domain \ - specification to requirements matching. +CredentialsStoreAction.GlobalDomainDisplayName=Global +CredentialsStoreAction.GlobalDomainDescription=Credentials that should be available everywhere. CredentialsStoreAction.EmptyDomainNameMessage=You must provide a name for the domain CredentialsStoreAction.DuplicateDomainNameMessage=A domain with that name already exists CredentialsStoreAction.UserDisplayName=User: {0} diff --git a/src/main/resources/com/cloudbees/plugins/credentials/ViewCredentialsAction/index.jelly b/src/main/resources/com/cloudbees/plugins/credentials/ViewCredentialsAction/index.jelly index 481629d61..76a9d433f 100644 --- a/src/main/resources/com/cloudbees/plugins/credentials/ViewCredentialsAction/index.jelly +++ b/src/main/resources/com/cloudbees/plugins/credentials/ViewCredentialsAction/index.jelly @@ -29,22 +29,70 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + - - - + + + + + + + + + +
@@ -86,7 +134,8 @@
- + @@ -95,8 +144,8 @@ - + href="${url}/move"/> +
- +
${%Delete credential}
@@ -126,7 +175,7 @@ - + @@ -142,7 +191,7 @@ - + diff --git a/src/main/resources/com/cloudbees/plugins/credentials/common/card.css b/src/main/resources/com/cloudbees/plugins/credentials/common/card.css index 4fd9addc2..6c74d1703 100644 --- a/src/main/resources/com/cloudbees/plugins/credentials/common/card.css +++ b/src/main/resources/com/cloudbees/plugins/credentials/common/card.css @@ -20,7 +20,7 @@ flex-direction: row; display: flex; gap: 0.5rem; - border-bottom: var(--jenkins-border); + border-bottom: var(--card-border-width) solid var(--card-border-color); &:last-of-type { border-bottom: none; @@ -97,4 +97,263 @@ flex-shrink: 0; margin-top: 0.0625rem; } -} \ No newline at end of file +} + +/*----*/ + +.credentials-descriptor-info { + display: grid; + grid-template-columns: auto 1fr; + gap: 0.25rem 0.875rem; + padding-bottom: 0.875rem; + border-bottom: var(--jenkins-border); + margin-bottom: 1.5rem; +} + +.jenkins-choice-list { + display: flex; + flex-direction: column; + padding: 0; + margin: 0; + border-radius: var(--form-input-border-radius); + border: var(--card-border-width) solid var(--card-border-color); + background: var(--card-background); + margin-bottom: var(--section-padding); + + .jenkins-choice-list__item { + -webkit-touch-callout: none; + position: relative; + user-select: none; + display: flex; + align-items: stretch; + flex-direction: column; + white-space: unset; + border-radius: 0; + cursor: pointer; + + &:not(:last-of-type) { + border-bottom: var(--card-border-width) solid var(--card-border-color); + } + + &::before { + content: ""; + position: absolute; + inset: 0; + border-radius: inherit; + transition: var(--standard-transition); + background: transparent; + box-shadow: + 0 0 0 0.09375rem transparent, + 0 0 0 0.5rem transparent; + pointer-events: none; + } + + &:hover { + &::before { + background: color-mix( + in sRGB, + var(--text-color-secondary) 7.5%, + transparent + ); + } + } + + &:active, + &:focus-visible { + z-index: 10; + border-radius: calc(var(--form-input-border-radius) / 2); + + &::before { + background: color-mix( + in sRGB, + var(--text-color-secondary) 12.5%, + transparent + ) !important; + box-shadow: + 0 0 0 0.09375rem + color-mix( + in sRGB, + var(--text-color-secondary) 20%, + var(--card-background) + ), + 0 0 0 0.34375rem + color-mix( + in sRGB, + color-mix( + in sRGB, + var(--text-color-secondary) 15%, + var(--card-background) + ) + 65%, + transparent + ); + } + } + + &:focus { + &::before { + box-shadow: 0 0 0 0.2rem var(--text-color) !important; + } + } + + &:has(input:checked) { + &::before { + background: color-mix( + in sRGB, + var(--text-color-secondary) 5%, + transparent + ); + } + } + + &:first-of-type { + border-top-left-radius: calc( + var(--form-input-border-radius) - var(--jenkins-border-width) + ) !important; + border-top-right-radius: calc( + var(--form-input-border-radius) - var(--jenkins-border-width) + ) !important; + } + + &:last-of-type { + border-bottom-left-radius: calc( + var(--form-input-border-radius) - var(--jenkins-border-width) + ) !important; + border-bottom-right-radius: calc( + var(--form-input-border-radius) - var(--jenkins-border-width) + ) !important; + } + } +} + +/*----*/ + +.jenkins-choice-list__item { + label { + display: grid; + grid-template-columns: auto 1fr; + gap: 0.25rem 0.875rem; + padding: 0.875rem; + cursor: pointer; + + &:has(input:checked) { + .jenkins-choice-list__item__icon { + color: var(--background); + + &::before { + box-shadow: + inset var(--jenkins-border--subtle-shadow), + inset 0 0 18px 18px var(--accent-color); + } + } + } + } + + label > input[type="radio"] { + display: none; + } +} + +.jenkins-choice-list__item__icon { + position: relative; + display: flex; + align-items: center; + justify-content: center; + height: 2.25rem; + width: 2.25rem; + grid-row: span 2; + z-index: 10; + transition: var(--standard-transition); + + &::before { + content: ""; + position: absolute; + inset: 0; + border-radius: var(--form-input-border-radius); + transition: var(--standard-transition); + z-index: 0; + background: rgb(from var(--text-color-secondary) r g b / 0.05); + box-shadow: + inset 0 0 0 var(--jenkins-border-width) var(--jenkins-border-color), + inset 0 0 0 var(--jenkins-border-width) + rgb(from var(--text-color-secondary) r g b / 0.05); + } + + svg, + img { + position: relative; + width: 1.25rem; + height: 1.25rem; + z-index: 1; + } +} + +.jenkins-choice-list__item__label { + display: block; + font-size: var(--font-size-sm); + font-weight: var(--font-bold-weight); + color: var(--text-color); +} + +/* + Workaround for multi line with icon not appearing well + TODO backport to core + */ +.jenkins-dropdown__item { + &:has(.jenkins-dropdown__item__description) { + display: grid; + grid-template-columns: auto 1fr; + row-gap: 0.25rem; + text-align: left; + overflow: clip; + text-wrap: wrap; + max-width: 380px; + + .jenkins-dropdown__item__description { + color: var(--text-color-secondary); + grid-area: 2 / 2; + line-height: 1.5; + } + } +} + +.jenkins-choice-list__item__description { + color: var(--text-color-secondary); + grid-column: 2; + padding-right: 2rem; + + white-space: initial; +} + +.jenkins-dialog__back-button { + position: absolute; + top: 1.25rem; + left: 1.25rem; + padding: 0; + width: 2rem; + min-height: 2rem; + border-radius: 100%; +} + +.jenkins-dialog__contents { + max-height: 85vh; + + &:has(#bottom-sticker) { + padding-bottom: 0; + } +} + +.cr-bottom-button { + width: 100%; +} + +/* TODO Backport to core */ +.jenkins-notice .jenkins-dropdown { + text-align: left; +} + +.jenkins-notice button > svg { + width: 1.125rem; + height: 1.125rem; +} + diff --git a/src/main/resources/lib/credentials/select.jelly b/src/main/resources/lib/credentials/select.jelly index aff5d386e..68f3b6919 100644 --- a/src/main/resources/lib/credentials/select.jelly +++ b/src/main/resources/lib/credentials/select.jelly @@ -111,28 +111,34 @@ - - + + - + - - - - + + + + + + + + @@ -140,7 +146,7 @@ @@ -157,4 +163,5 @@
+ diff --git a/src/main/resources/lib/credentials/select/select.css b/src/main/resources/lib/credentials/select/select.css index 89b59caf7..c11ba35f7 100644 --- a/src/main/resources/lib/credentials/select/select.css +++ b/src/main/resources/lib/credentials/select/select.css @@ -10,3 +10,10 @@ div.credentials-select-content-inactive { grid-template-columns: 1fr auto; gap: calc(var(--section-padding) * 0.5); } + +.cr-select-description { + color: var(--text-color-secondary); + margin-top: 0.4rem; + + white-space: initial; +} diff --git a/src/main/resources/lib/credentials/select/select.js b/src/main/resources/lib/credentials/select/select.js index 06630b19f..0e8b7ce53 100644 --- a/src/main/resources/lib/credentials/select/select.js +++ b/src/main/resources/lib/credentials/select/select.js @@ -22,36 +22,148 @@ * THE SOFTWARE. */ window.credentials = window.credentials || {'dialog': null, 'body': null}; -window.credentials.init = function () { - if (!(window.credentials.dialog)) { - var div = document.createElement("DIV"); - document.body.appendChild(div); - div.innerHTML = "
"; - window.credentials.body = document.getElementById('credentialsDialog'); + +function showBackButtonInDialog() { + const dialog = document.querySelector(".jenkins-dialog"); + const title = dialog.querySelector(".jenkins-dialog__title"); + const backButton = document.createElement("button"); + backButton.classList.add("jenkins-button"); + backButton.classList.add("jenkins-dialog__back-button"); + backButton.innerHTML = ``; + title.style.transition = "var(--standard-transition)"; + title.style.marginLeft = "2.75rem"; + dialog.appendChild(backButton); + + backButton.addEventListener("click", () => { + dialog.querySelector(".jenkins-dialog__contents form:first-of-type").classList.remove("jenkins-hidden"); + dialog.querySelector(".jenkins-dialog__contents form:last-of-type").remove(); + title.style.marginLeft = "0"; + title.textContent = "Add Credentials"; + backButton.remove(); + }) +} + +/* + * Recreate script tags to ensure they are executed, as innerHTML does not execute scripts. + */ +function recreateScripts(form) { + const scripts = form.getElementsByTagName("script"); + if (scripts.length === 0) { + Behaviour.applySubtree(form, true); + return; } -}; -window.credentials.add = function (e) { - window.credentials.init(); - fetch(e, { + for (let i = 0; i < scripts.length; i++) { + const script = document.createElement("script"); + if (scripts[i].text) { + script.text = scripts[i].text; + } else { + for (let j = 0; j < scripts[i].attributes.length; j++) { + if (scripts[i].attributes[j].name in HTMLScriptElement.prototype) { + script[scripts[i].attributes[j].name] = scripts[i].attributes[j].value; + } + } + } + + // only attach the load listener to the last script to avoid multiple calls to Behaviour.applySubtree + if (i === (scripts.length - 1)) { + script.addEventListener("load", () => { + Behaviour.applySubtree(form, true); + if (form.method.toLowerCase() !== 'get') { + form.onsubmit = null; // clear any existing handler + } + }) + } + + scripts[i].parentNode.replaceChild(script, scripts[i]); + } +} + +function mergeUrlParams(url, params) { + const base = new URL(url, window.location.href); + if (params) { + const newParams = new URLSearchParams(params); + for (const [key, value] of newParams.entries()) { + base.searchParams.set(key, value); + } + } + return base.toString(); +} + +function navigateToNextPage(url, params) { + const dialog = document.querySelector(".jenkins-dialog .jenkins-dialog__contents"); + + const finalUrl = mergeUrlParams(url, params); + + fetch(finalUrl, { method: 'GET', headers: crumb.wrap({}), }).then(rsp => { if (rsp.ok) { rsp.text().then((responseText) => { - // do not apply behaviour on parsed HTML, dialog.form does that later - // otherwise we have crumb and json fields twice - window.credentials.body.innerHTML = responseText; - window.credentials.form = document.getElementById('credentials-dialog-form'); - const data = window.credentials.form.dataset; - const options = {'title': data['title'], 'okText': data['add'], 'submitButton':false, 'minWidth': 'min(1641px, 65vw)'}; - dialog.form(window.credentials.form, options) - .then(window.credentials.addSubmit); - window.credentials.form.querySelector('select').focus(); - }); + Array.from(dialog.children) + .filter(el => el.tagName === "FORM") + .forEach(form => form.classList.add("jenkins-hidden")); + + const newDialog = document.createElement("div"); + newDialog.innerHTML = responseText; + + const form = newDialog.querySelector("form"); + + const title = document.querySelector(".jenkins-dialog .jenkins-dialog__title"); + title.textContent = rsp.headers.get("X-Wizard-Title"); + + if (form.method.toLowerCase() === 'get') { + form.addEventListener("submit", (e) => { + e.preventDefault(); + + const form = e.target; + const fd = new FormData(form); + const params = new URLSearchParams(); + + fd.forEach(function (value, key) { + // FormData can include File objects. Query strings cannot. + if (value instanceof File) { + // choose one: + // params.append(key, value.name); // store filename only + return; // or skip files entirely + } + params.append(key, String(value)); + }); + + const queryString = params.toString(); // "username=alice&password=secret" + + showBackButtonInDialog(); + + navigateToNextPage(form.action, queryString); + }) + } else { + window.credentials.form = form + form.addEventListener("submit", (e) => { + e.preventDefault(); + window.credentials.addSubmit(); + }) + } + + dialog.appendChild(form) + recreateScripts(form) + }) } - }); + }) +} + +window.dialog2 = { + wizard: (initialUrl, options) => { + dialog.modal(document.createElement("template"), options); + + navigateToNextPage(initialUrl, ''); + } +}; + +window.credentials.add = function (initialUrl) { + window.dialog2.wizard(initialUrl, { title: '', minWidth: 'min(550px, 100vw)' }); return false; }; + window.credentials.refreshAll = function () { document.querySelectorAll('select.credentials-select').forEach(function (e) { var deps = []; @@ -103,6 +215,7 @@ window.credentials.refreshAll = function () { }; window.credentials.addSubmit = function (_) { const form = window.credentials.form; + const shouldRefill = document.getElementById('credentials-dialog-refill') // temporarily attach to DOM (avoid https://github.com/HtmlUnit/htmlunit/issues/740) document.body.appendChild(form); buildFormTree(form); @@ -112,13 +225,20 @@ window.credentials.addSubmit = function (_) { function ajaxFormSubmit(form) { fetch(form.action, { method: form.method, - headers: crumb.wrap({}), + headers: crumb.wrap({"Accept": "application/json"}), body: new FormData(form) }) .then(res => res.json()) - .then(data => { - window.notificationBar.show(data.message, window.notificationBar[data.notificationType]); - window.credentials.refreshAll(); + .then(result => { + window.notificationBar.show(result.data.message, window.notificationBar[result.data.notificationType]); + const dialog = document.querySelector(".jenkins-dialog"); + dialog.dispatchEvent(new Event("cancel")); + // when used in `c:select` we don't want a page reload but to instead refill existing select boxes + if (shouldRefill && shouldRefill.dataset.refill === 'true') { + window.credentials.refreshAll(); + } else { + window.location.reload(); + } }) .catch((e) => { // notificationBar.show(...) with logging ID could be handy here? @@ -130,7 +250,7 @@ window.credentials.addSubmit = function (_) { Behaviour.specify("[data-type='credentials-add-store-item']", 'credentials-add-store-item', -99, function(e) { e.addEventListener("click", function (event) { - window.credentials.add(event.target.dataset.url); + window.credentials.add(event.target.closest('button').dataset.url); }); e = null; }); @@ -203,3 +323,10 @@ window.setTimeout(function() { Behaviour.applySubtree(controls[i], true); } },1); + +// Enable the "Next" button when a radio button is selected +Behaviour.specify(".jenkins-choice-list__item input[type='radio']", 'choice-radio', 0, function (e) { + e.addEventListener("change", function () { + e.closest("form").querySelector("button").disabled = false; + }) +}); diff --git a/src/main/resources/lib/credentials/store.jelly b/src/main/resources/lib/credentials/store.jelly index fc00c8499..5d6853eb3 100644 --- a/src/main/resources/lib/credentials/store.jelly +++ b/src/main/resources/lib/credentials/store.jelly @@ -31,24 +31,17 @@ THE SOFTWARE.
diff --git a/src/test/java/com/cloudbees/plugins/credentials/CredentialsSelectHelperTest.java b/src/test/java/com/cloudbees/plugins/credentials/CredentialsSelectHelperTest.java index 4bd099da7..b15802773 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/CredentialsSelectHelperTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/CredentialsSelectHelperTest.java @@ -5,11 +5,14 @@ import static org.hamcrest.Matchers.is; import static org.junit.jupiter.api.Assertions.assertTrue; +import com.cloudbees.plugins.credentials.common.CertificateCredentials; +import com.cloudbees.plugins.credentials.common.StandardCertificateCredentials; import com.cloudbees.plugins.credentials.common.UsernamePasswordCredentials; import com.cloudbees.plugins.credentials.impl.CertificateCredentialsImpl; import com.cloudbees.plugins.credentials.impl.CertificateCredentialsImplTest; import hudson.model.UnprotectedRootAction; +import hudson.security.ACL; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.List; @@ -20,6 +23,7 @@ import org.htmlunit.html.DomNode; import org.htmlunit.html.DomNodeList; import org.htmlunit.html.HtmlButton; +import org.htmlunit.html.HtmlDivision; import org.htmlunit.html.HtmlElementUtil; import org.htmlunit.html.HtmlForm; import org.htmlunit.html.HtmlFormUtil; @@ -61,14 +65,17 @@ void doAddCredentialsFromPopupWorksAsExpected() throws Exception { HtmlPage htmlPage = wc.goTo("credentials-selection"); HtmlButton addCredentialsButton = htmlPage.querySelector(".credentials-add-menu"); - // The 'click' event doesn't fire a 'mouseenter' event causing the menu not to show, so let's fire one - addCredentialsButton.fireEvent("mouseenter"); addCredentialsButton.click(); HtmlButton jenkinsCredentialsOption = htmlPage.querySelector(".jenkins-dropdown__item"); - jenkinsCredentialsOption.click(); + HtmlElementUtil.click(jenkinsCredentialsOption); + + HtmlRadioButtonInput item = htmlPage.querySelector(".jenkins-choice-list__item input"); + HtmlElementUtil.click(item); + + HtmlButton formSubmitButton = htmlPage.querySelector("#cr-dialog-next"); + HtmlElementUtil.click(formSubmitButton); - wc.waitForBackgroundJavaScript(4000); HtmlForm form = htmlPage.querySelector("#credentials-dialog-form"); HtmlInput username = form.querySelector("input[name='_.username']"); @@ -78,12 +85,11 @@ void doAddCredentialsFromPopupWorksAsExpected() throws Exception { HtmlInput id = form.querySelector("input[name='_.id']"); id.setValue("test"); - HtmlButton formSubmitButton = htmlPage.querySelector(".jenkins-button[data-id='ok']"); - formSubmitButton.fireEvent("click"); - wc.waitForBackgroundJavaScript(5000); + formSubmitButton = htmlPage.querySelector("#cr-dialog-submit"); + HtmlElementUtil.click(formSubmitButton); // check if credentials were added - List creds = CredentialsProvider.lookupCredentials(UsernamePasswordCredentials.class); + List creds = CredentialsProvider.lookupCredentialsInItem(UsernamePasswordCredentials.class, null, ACL.SYSTEM2); assertThat(creds, Matchers.hasSize(1)); UsernamePasswordCredentials cred = creds.get(0); assertThat(cred.getUsername(), is("bob")); @@ -98,12 +104,19 @@ void doAddCredentialsFromPopupForPEMCertificateKeystore() throws Exception { try (JenkinsRule.WebClient wc = j.createWebClient()) { HtmlPage htmlPage = wc.goTo("credentials-selection"); HtmlForm form = selectPEMCertificateKeyStore(htmlPage, wc); + HtmlInput id = form.querySelector("input[name='_.id']"); + id.setValue("test"); form.getTextAreaByName("_.certChain").setTextContent(pemCert); form.getTextAreaByName("_.privateKey").setTextContent(pemKey); form.getInputsByName("_.password").forEach(input -> input.setValue(VALID_PASSWORD)); - Page submit = HtmlFormUtil.submit(form); - JSONObject responseJson = JSONObject.fromObject(submit.getWebResponse().getContentAsString()); - assertThat(responseJson.getString("notificationType"), is("SUCCESS")); + + HtmlButton formSubmitButton = htmlPage.querySelector("#cr-dialog-submit"); + HtmlElementUtil.click(formSubmitButton); + + List creds = CredentialsProvider.lookupCredentialsInItem(StandardCertificateCredentials.class, null, ACL.SYSTEM2); + assertThat(creds, Matchers.hasSize(1)); + StandardCertificateCredentials cred = creds.get(0); + assertThat(cred.getId(), is("test")); } } @@ -113,10 +126,13 @@ void doAddCredentialsFromPopupForPEMCertificateKeystore_missingKeyStore() throws try (JenkinsRule.WebClient wc = j.createWebClient()) { HtmlPage htmlPage = wc.goTo("credentials-selection"); - HtmlForm form = selectPEMCertificateKeyStore(htmlPage, wc); - Page submit = HtmlFormUtil.submit(form); - JSONObject responseJson = JSONObject.fromObject(submit.getWebResponse().getContentAsString()); - assertThat(responseJson.getString("notificationType"), is("ERROR")); + selectPEMCertificateKeyStore(htmlPage, wc); + + HtmlButton formSubmitButton = htmlPage.querySelector("#cr-dialog-submit"); + HtmlElementUtil.click(formSubmitButton); + + List creds = CredentialsProvider.lookupCredentialsInItem(StandardCertificateCredentials.class, null, ACL.SYSTEM2); + assertThat(creds, Matchers.hasSize(0)); } } @@ -130,9 +146,12 @@ void doAddCredentialsFromPopupForInvalidPEMCertificateKeystore_missingCert() thr form.getTextAreaByName("_.certChain").setTextContent(null); form.getTextAreaByName("_.privateKey").setTextContent(pemKey); form.getInputsByName("_.password").forEach(input -> input.setValue(VALID_PASSWORD)); - Page submit = HtmlFormUtil.submit(form); - JSONObject responseJson = JSONObject.fromObject(submit.getWebResponse().getContentAsString()); - assertThat(responseJson.getString("notificationType"), is("ERROR")); + + HtmlButton formSubmitButton = htmlPage.querySelector("#cr-dialog-submit"); + HtmlElementUtil.click(formSubmitButton); + + List creds = CredentialsProvider.lookupCredentialsInItem(StandardCertificateCredentials.class, null, ACL.SYSTEM2); + assertThat(creds, Matchers.hasSize(0)); } } @@ -145,9 +164,12 @@ void doAddCredentialsFromPopupForInvalidPEMCertificateKeystore_missingPassword() HtmlForm form = selectPEMCertificateKeyStore(htmlPage, wc); form.getTextAreaByName("_.certChain").setTextContent(pemCert); form.getTextAreaByName("_.privateKey").setTextContent(pemKey); - Page submit = HtmlFormUtil.submit(form); - JSONObject responseJson = JSONObject.fromObject(submit.getWebResponse().getContentAsString()); - assertThat(responseJson.getString("notificationType"), is("ERROR")); + + HtmlButton formSubmitButton = htmlPage.querySelector("#cr-dialog-submit"); + HtmlElementUtil.click(formSubmitButton); + + List creds = CredentialsProvider.lookupCredentialsInItem(StandardCertificateCredentials.class, null, ACL.SYSTEM2); + assertThat(creds, Matchers.hasSize(0)); } } @@ -161,44 +183,54 @@ void doAddCredentialsFromPopupForInvalidPEMCertificateKeystore_invalidPassword() form.getTextAreaByName("_.certChain").setTextContent(pemCert); form.getTextAreaByName("_.privateKey").setTextContent(pemKey); form.getInputsByName("_.password").forEach(input -> input.setValue(INVALID_PASSWORD)); - Page submit = HtmlFormUtil.submit(form); - JSONObject responseJson = JSONObject.fromObject(submit.getWebResponse().getContentAsString()); - assertThat(responseJson.getString("notificationType"), is("ERROR")); + + HtmlButton formSubmitButton = htmlPage.querySelector("#cr-dialog-submit"); + HtmlElementUtil.click(formSubmitButton); + + List creds = CredentialsProvider.lookupCredentialsInItem(StandardCertificateCredentials.class, null, ACL.SYSTEM2); + assertThat(creds, Matchers.hasSize(0)); } } private HtmlForm selectPEMCertificateKeyStore(HtmlPage htmlPage, JenkinsRule.WebClient wc) throws IOException { HtmlButton addCredentialsButton = htmlPage.querySelector(".credentials-add-menu"); - addCredentialsButton.fireEvent("mouseenter"); addCredentialsButton.click(); HtmlButton jenkinsCredentialsOption = htmlPage.querySelector(".jenkins-dropdown__item"); - jenkinsCredentialsOption.click(); + HtmlElementUtil.click(jenkinsCredentialsOption); - wc.waitForBackgroundJavaScript(4000); - HtmlForm form = htmlPage.querySelector("#credentials-dialog-form"); + HtmlForm form = htmlPage.getFormByName("dialog"); String certificateDisplayName = j.jenkins.getDescriptor(CertificateCredentialsImpl.class).getDisplayName(); String KeyStoreSourceDisplayName = j.jenkins.getDescriptor( CertificateCredentialsImpl.PEMEntryKeyStoreSource.class).getDisplayName(); - DomNodeList allOptions = htmlPage.getDocumentElement().querySelectorAll( - "select.dropdownList option"); + + DomNodeList allOptions = form.querySelectorAll(".jenkins-choice-list__item"); + boolean optionFound = selectOption(allOptions, certificateDisplayName); assertTrue(optionFound, "The Certificate option was not found in the credentials type select"); + + HtmlButton formSubmitButton = htmlPage.querySelector("#cr-dialog-next"); + HtmlElementUtil.click(formSubmitButton); + List inputs = htmlPage.getDocumentElement().getByXPath( "//input[contains(@name, 'keyStoreSource') and following-sibling::label[contains(.,'" + KeyStoreSourceDisplayName + "')]]"); assertThat("query should return only a singular input", inputs, hasSize(1)); HtmlElementUtil.click(inputs.get(0)); wc.waitForBackgroundJavaScript(4000); + form = htmlPage.getFormByName("newCredentials"); + return form; } private static boolean selectOption(DomNodeList allOptions, String optionName) { return allOptions.stream().anyMatch(domNode -> { - if (domNode instanceof HtmlOption option) { - if (option.getVisibleText().equals(optionName)) { + if (domNode instanceof HtmlDivision option) { + if (option.getVisibleText().contains(optionName)) { try { - HtmlElementUtil.click(option); + HtmlRadioButtonInput item = domNode.querySelector(".jenkins-choice-list__item input"); + HtmlElementUtil.click(item); + } catch (IOException e) { throw new RuntimeException(e); } diff --git a/src/test/java/com/cloudbees/plugins/credentials/CredentialsStoreActionTest.java b/src/test/java/com/cloudbees/plugins/credentials/CredentialsStoreActionTest.java index ada6c8a27..f270971ca 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/CredentialsStoreActionTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/CredentialsStoreActionTest.java @@ -64,11 +64,10 @@ void smokes() throws Exception { + "" + "<_>" + "" - + "Credentials that should be available irrespective of domain specification to requirements " - + "matching." + + "Credentials that should be available everywhere." + "" - + "Global credentials (unrestricted)" - + "System ยป Global credentials (unrestricted)" + + "Global" + + "System ยป Global" + "system/_" + "true" + "_" @@ -90,11 +89,10 @@ void smokes() throws Exception { + "" + "<_>" + "" - + "Credentials that should be available irrespective of domain specification to requirements " - + "matching." + + "Credentials that should be available everywhere." + "" - + "Global credentials (unrestricted)" - + "System ยป Global credentials (unrestricted)" + + "Global" + + "System ยป Global" + "system/_" + "true" + "_" diff --git a/src/test/java/com/cloudbees/plugins/credentials/ViewCredentialsActionTest.java b/src/test/java/com/cloudbees/plugins/credentials/ViewCredentialsActionTest.java index 4e7e6c45f..214645145 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/ViewCredentialsActionTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/ViewCredentialsActionTest.java @@ -54,11 +54,10 @@ void smokes(JenkinsRule j) throws Exception { + "" + "<_>" + "" - + "Credentials that should be available irrespective of domain specification to requirements " - + "matching." + + "Credentials that should be available everywhere." + "" - + "Global credentials (unrestricted)" - + "System ยป Global credentials (unrestricted)" + + "Global" + + "System ยป Global" + "system/_" + "true" + "_" @@ -84,11 +83,10 @@ void smokes(JenkinsRule j) throws Exception { + "" + "<_>" + "" - + "Credentials that should be available irrespective of domain specification to requirements " - + "matching." + + "Credentials that should be available everywhere." + "" - + "Global credentials (unrestricted)" - + "System ยป Global credentials (unrestricted)" + + "Global" + + "System ยป Global" + "system/_" + "true" + "_" diff --git a/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java b/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java index 658fa3711..141245293 100644 --- a/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java +++ b/src/test/java/com/cloudbees/plugins/credentials/impl/CertificateCredentialsImplTest.java @@ -37,6 +37,8 @@ import org.htmlunit.html.DomNode; import org.htmlunit.html.DomNodeList; import org.htmlunit.html.HtmlButton; +import org.htmlunit.html.HtmlDivision; +import org.htmlunit.html.HtmlElement; import org.htmlunit.html.HtmlElementUtil; import org.htmlunit.html.HtmlFileInput; import org.htmlunit.html.HtmlForm; @@ -206,26 +208,25 @@ void fullSubmitOfUploadedKeystore() throws Exception { String KeyStoreSourceDisplayName = r.jenkins.getDescriptor(CertificateCredentialsImpl.UploadedKeyStoreSource.class).getDisplayName(); JenkinsRule.WebClient wc = r.createWebClient(); - HtmlPage htmlPage = wc.goTo("credentials/store/system/domain/_/newCredentials"); - HtmlForm newCredentialsForm = htmlPage.getFormByName("newCredentials"); + HtmlPage htmlPage = wc.goTo("credentials/store/system/domain/_/"); - DomNodeList allOptions = htmlPage.getDocumentElement().querySelectorAll("select.dropdownList option"); - boolean optionFound = allOptions.stream().anyMatch(domNode -> { - if (domNode instanceof HtmlOption option) { - if (option.getVisibleText().equals(certificateDisplayName)) { - try { - HtmlElementUtil.click(option); - } catch (IOException e) { - throw new RuntimeException(e); - } - return true; - } - } + HtmlButton button = (HtmlButton) htmlPage + .getDocumentElement() + .getElementsByAttribute("button", "data-type", "credentials-add-store-item").get(0); + HtmlElementUtil.click(button); + + HtmlForm form = htmlPage.getFormByName("dialog"); + + DomNodeList allOptions = form.querySelectorAll(".jenkins-choice-list__item"); + boolean optionFound = selectOption(allOptions, certificateDisplayName); - return false; - }); assertTrue(optionFound, "The Certificate option was not found in the credentials type select"); + HtmlButton formSubmitButton = htmlPage.querySelector("#cr-dialog-next"); + HtmlElementUtil.click(formSubmitButton); + + HtmlForm newCredentialsForm = htmlPage.getFormByName("newCredentials"); + List inputs = htmlPage.getDocumentElement(). getByXPath("//input[contains(@name, 'keyStoreSource') and following-sibling::label[contains(.,'"+KeyStoreSourceDisplayName+"')]]"); assertThat("query should return only a singular input", inputs, hasSize(1)); @@ -241,7 +242,8 @@ void fullSubmitOfUploadedKeystore() throws Exception { List certificateCredentials = CredentialsProvider.lookupCredentialsInItemGroup(CertificateCredentials.class, null, ACL.SYSTEM2); assertThat(certificateCredentials, hasSize(0)); - r.submit(newCredentialsForm); + formSubmitButton = htmlPage.querySelector("#cr-dialog-submit"); + HtmlElementUtil.click(formSubmitButton); certificateCredentials = CredentialsProvider.lookupCredentialsInItemGroup(CertificateCredentials.class, null, ACL.SYSTEM2); assertThat(certificateCredentials, hasSize(1)); @@ -258,36 +260,34 @@ void fullSubmitOfUploadedPEM() throws Exception { String KeyStoreSourceDisplayName = r.jenkins.getDescriptor(CertificateCredentialsImpl.PEMEntryKeyStoreSource.class).getDisplayName(); JenkinsRule.WebClient wc = r.createWebClient(); - HtmlPage htmlPage = wc.goTo("credentials/store/system/domain/_/newCredentials"); - HtmlForm newCredentialsForm = htmlPage.getFormByName("newCredentials"); + HtmlPage htmlPage = wc.goTo("credentials/store/system/domain/_/"); - DomNodeList allOptions = htmlPage.getDocumentElement().querySelectorAll("select.dropdownList option"); - boolean optionFound = allOptions.stream().anyMatch(domNode -> { - if (domNode instanceof HtmlOption option) { - if (option.getVisibleText().equals(certificateDisplayName)) { - try { - HtmlElementUtil.click(option); - } catch (IOException e) { - throw new RuntimeException(e); - } - return true; - } - } + HtmlButton button = (HtmlButton) htmlPage + .getDocumentElement() + .getElementsByAttribute("button", "data-type", "credentials-add-store-item").get(0); + HtmlElementUtil.click(button); - return false; - }); + HtmlForm form = htmlPage.getFormByName("dialog"); + + DomNodeList allOptions = form.querySelectorAll(".jenkins-choice-list__item"); + boolean optionFound = selectOption(allOptions, certificateDisplayName); assertTrue(optionFound, "The Certificate option was not found in the credentials type select"); + HtmlButton formSubmitButton = htmlPage.querySelector("#cr-dialog-next"); + HtmlElementUtil.click(formSubmitButton); + + HtmlForm newCredentialsForm = htmlPage.getFormByName("newCredentials"); + List inputs = htmlPage.getDocumentElement(). getByXPath("//input[contains(@name, 'keyStoreSource') and following-sibling::label[contains(.,'"+KeyStoreSourceDisplayName+"')]]"); assertThat("query should return only a singular input", inputs, hasSize(1)); HtmlElementUtil.click(inputs.get(0)); // enable entry of the secret (HACK just click all the Add buttons) - List buttonsByName = htmlPage.getDocumentElement().getByXPath("//button[contains(.,'Add')]"); + DomNodeList buttonsByName = newCredentialsForm.querySelectorAll(".secret-update-btn"); assertThat("I need 2 buttons", buttonsByName, hasSize(2)); - for (HtmlButton b : buttonsByName) { - HtmlElementUtil.click(b); + for (DomNode b : buttonsByName) { + HtmlElementUtil.click((HtmlElement) b); } newCredentialsForm.getTextAreaByName("_.certChain").setTextContent(pemCert); @@ -310,6 +310,24 @@ void fullSubmitOfUploadedPEM() throws Exception { assertEquals(EXPECTED_DISPLAY_NAME_PEM, displayName); } + private static boolean selectOption(DomNodeList allOptions, String optionDisplayName) { + return allOptions.stream().anyMatch(domNode -> { + if (domNode instanceof HtmlDivision option) { + if (option.getVisibleText().contains(optionDisplayName)) { + try { + HtmlRadioButtonInput item = domNode.querySelector(".jenkins-choice-list__item input"); + HtmlElementUtil.click(item); + } catch (IOException e) { + throw new RuntimeException(e); + } + return true; + } + } + + return false; + }); + } + private String getValidP12_base64() throws Exception { return Base64.getEncoder().encodeToString(Files.readAllBytes(p12.toPath())); }