From c4785930e17a95076360154f1cbc0c5ed026cd2c Mon Sep 17 00:00:00 2001 From: Itzik Bekel <119784889+itzikbekelmicrosoft@users.noreply.github.com> Date: Wed, 17 Jul 2024 13:48:04 +0300 Subject: [PATCH] Add client certificate support (#596) Co-authored-by: Tim Jacomb <21194782+timja@users.noreply.github.com> --- README.md | 8 +- .../jenkins/azuread/AzureSecurityRealm.java | 113 +++++++++++++++--- .../jenkins/azuread/GraphClientCache.java | 52 ++++++-- .../jenkins/azuread/GraphClientCacheKey.java | 26 ++-- .../azuread/AzureSecurityRealm/config.jelly | 29 +++-- .../help-cacheDuration.html} | 0 .../help-clientCertificate.html | 14 +++ .../AzureSecurityRealm/help-clientId.html} | 0 .../help-clientSecret.html} | 0 .../AzureSecurityRealm/help-fromRequest.html} | 0 .../AzureSecurityRealm}/help-tenant.html | 0 .../jenkins/azuread/Messages.properties | 1 + .../azuread/AzureAdConfigurationSaveTest.java | 60 +++++++--- .../azuread/AzureSecurityRealmTest.java | 35 +++++- ...odeTest.java => BaseConfigAsCodeTest.java} | 58 +-------- .../ConfigAsCodeClientCertificateTest.java | 37 ++++++ .../casc/ConfigAsCodeClientSecretTest.java | 37 ++++++ .../ConfigAsCodeExportConfigurationTest.java | 32 +++++ .../casc/ConfigAsCodeTest/config.xml | 2 + ...configuration-as-code-certificate-auth.yml | 97 +++++++++++++++ ... => configuration-as-code-secret-auth.yml} | 0 21 files changed, 483 insertions(+), 118 deletions(-) rename src/main/{webapp/help/help-cache-duration.html => resources/com/microsoft/jenkins/azuread/AzureSecurityRealm/help-cacheDuration.html} (100%) create mode 100644 src/main/resources/com/microsoft/jenkins/azuread/AzureSecurityRealm/help-clientCertificate.html rename src/main/{webapp/help/help-client-id.html => resources/com/microsoft/jenkins/azuread/AzureSecurityRealm/help-clientId.html} (100%) rename src/main/{webapp/help/help-client-secret.html => resources/com/microsoft/jenkins/azuread/AzureSecurityRealm/help-clientSecret.html} (100%) rename src/main/{webapp/help/help-from-request.html => resources/com/microsoft/jenkins/azuread/AzureSecurityRealm/help-fromRequest.html} (100%) rename src/main/{webapp/help => resources/com/microsoft/jenkins/azuread/AzureSecurityRealm}/help-tenant.html (100%) rename src/test/java/com/microsoft/jenkins/azuread/integrations/casc/{ConfigAsCodeTest.java => BaseConfigAsCodeTest.java} (65%) create mode 100644 src/test/java/com/microsoft/jenkins/azuread/integrations/casc/ConfigAsCodeClientCertificateTest.java create mode 100644 src/test/java/com/microsoft/jenkins/azuread/integrations/casc/ConfigAsCodeClientSecretTest.java create mode 100644 src/test/java/com/microsoft/jenkins/azuread/integrations/casc/ConfigAsCodeExportConfigurationTest.java create mode 100644 src/test/resources/com/microsoft/jenkins/azuread/integrations/casc/configuration-as-code-certificate-auth.yml rename src/test/resources/com/microsoft/jenkins/azuread/integrations/casc/{configuration-as-code.yml => configuration-as-code-secret-auth.yml} (100%) diff --git a/README.md b/README.md index 23363bcd..1d5b33d4 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,11 @@ A Jenkins Plugin that supports authentication & authorization via Microsoft Entr 1. Add a new Reply URL `https://{your_jenkins_host}/securityRealm/finishLogin`. Make sure "Jenkins URL" (Manage Jenkins => Configure System) is set to the same value as `https://{your_jenkins_host}`. -1. Click `Certificates & secrets`, under Client secrets click `New client secret` to generate a new key, copy the `value`, it will be used as `Client Secret` in Jenkins. +1. Click `Certificates & secrets` + + - To use a client secret: Under Client secrets, click `New client secret` to generate a new key. Copy the `value`, it will be used as `Client Secret` in Jenkins. + + - To use a certificate: Under Certificates, click `Upload certificate` to upload your certificate. This certificate will be used for client certificate authentication in Jenkins. You will need to use the corresponding private key associated with this certificate in PEM format. 1. Click `Authentication`, under 'Implicit grant and hybrid flows', enable `ID tokens`. @@ -98,4 +102,4 @@ A: You can disable the security from the config file (see [https://www.jenkins.i #### Q: Why am I getting an error "insufficient privileges to complete the operation" even after having granted the permission? -A: It can take a long time for the privileges to take effect, which could be 10-20 minutes. Just wait for a while and try again. +A: It can take a long time for the privileges to take effect, which could be 10-20 minutes. Just wait for a while and try again. \ No newline at end of file diff --git a/src/main/java/com/microsoft/jenkins/azuread/AzureSecurityRealm.java b/src/main/java/com/microsoft/jenkins/azuread/AzureSecurityRealm.java index 7a937385..c16c5c4e 100644 --- a/src/main/java/com/microsoft/jenkins/azuread/AzureSecurityRealm.java +++ b/src/main/java/com/microsoft/jenkins/azuread/AzureSecurityRealm.java @@ -9,15 +9,15 @@ import com.azure.core.credential.TokenRequestContext; import com.azure.identity.ClientSecretCredential; import com.azure.identity.ClientSecretCredentialBuilder; +import com.azure.identity.ClientCertificateCredential; +import com.azure.identity.ClientCertificateCredentialBuilder; import com.github.benmanes.caffeine.cache.Cache; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.scribejava.core.builder.ServiceBuilder; import com.github.scribejava.core.oauth.OAuth20Service; import com.google.common.base.Supplier; import com.google.common.base.Suppliers; -import com.microsoft.graph.authentication.TokenCredentialAuthProvider; import com.microsoft.graph.http.GraphServiceException; -import com.microsoft.graph.httpcore.HttpClients; import com.microsoft.graph.models.Group; import com.microsoft.graph.options.Option; import com.microsoft.graph.options.QueryOption; @@ -33,7 +33,6 @@ import edu.umd.cs.findbugs.annotations.CheckForNull; import edu.umd.cs.findbugs.annotations.NonNull; import hudson.Extension; -import hudson.ProxyConfiguration; import hudson.Util; import hudson.model.Descriptor; import hudson.model.User; @@ -52,9 +51,6 @@ import jenkins.model.Jenkins; import jenkins.security.SecurityListener; -import jenkins.util.JenkinsJVM; -import okhttp3.Credentials; -import okhttp3.OkHttpClient; import okhttp3.Request; import org.apache.commons.lang3.RandomStringUtils; import org.apache.commons.lang3.StringUtils; @@ -79,9 +75,10 @@ import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; import java.io.UnsupportedEncodingException; -import java.net.Proxy; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; @@ -113,6 +110,8 @@ public class AzureSecurityRealm extends SecurityRealm { public static final String CALLBACK_URL = "/securityRealm/finishLogin"; private static final String CONVERTER_NODE_CLIENT_ID = "clientid"; private static final String CONVERTER_NODE_CLIENT_SECRET = "clientsecret"; + private static final String CONVERTER_NODE_CLIENT_CERTIFICATE = "clientCertificate"; + private static final String CONVERTER_NODE_CREDENTIAL_TYPE = "credentialType"; private static final String CONVERTER_NODE_TENANT = "tenant"; private static final String CONVERTER_NODE_CACHE_DURATION = "cacheduration"; private static final String CONVERTER_NODE_FROM_REQUEST = "fromrequest"; @@ -122,12 +121,14 @@ public class AzureSecurityRealm extends SecurityRealm { public static final String CONVERTER_DISABLE_GRAPH_INTEGRATION = "disableGraphIntegration"; public static final String CONVERTER_SINGLE_LOGOUT = "singleLogout"; public static final String CONVERTER_PROMPT_ACCOUNT = "promptAccount"; + public static final String CONVERTER_ENVIRONMENT_NAME = "environmentName"; private Cache<String, AzureAdUser> caches; private Secret clientId; private Secret clientSecret; + private Secret clientCertificate; private Secret tenant; private int cacheDuration; private boolean fromRequest = false; @@ -135,15 +136,16 @@ public class AzureSecurityRealm extends SecurityRealm { private boolean singleLogout; private boolean disableGraphIntegration; private String azureEnvironmentName = "Azure"; + private String credentialType = "Secret"; public AccessToken getAccessToken() { - ClientSecretCredential clientSecretCredential = getClientSecretCredential(); - TokenRequestContext tokenRequestContext = new TokenRequestContext(); String graphResource = AzureEnvironment.getGraphResource(getAzureEnvironmentName()); tokenRequestContext.setScopes(singletonList(graphResource + ".default")); - AccessToken accessToken = clientSecretCredential.getToken(tokenRequestContext).block(); + AccessToken accessToken = ("Certificate".equals(credentialType) ? getClientCertificateCredential() : getClientSecretCredential()) + .getToken(tokenRequestContext) + .block(); if (accessToken == null) { throw new IllegalStateException("Access token null when it is required"); @@ -152,6 +154,13 @@ public AccessToken getAccessToken() { return accessToken; } + InputStream getCertificate() { + + String secretString = clientCertificate.getPlainText(); + + return new ByteArrayInputStream(secretString.getBytes(StandardCharsets.UTF_8)); + } + ClientSecretCredential getClientSecretCredential() { String azureEnv = getAzureEnvironmentName(); return new ClientSecretCredentialBuilder() @@ -163,6 +172,17 @@ ClientSecretCredential getClientSecretCredential() { .build(); } + ClientCertificateCredential getClientCertificateCredential() { + String azureEnv = getAzureEnvironmentName(); + return new ClientCertificateCredentialBuilder() + .clientId(clientId.getPlainText()) + .pemCertificate(getCertificate()) + .tenantId(tenant.getPlainText()) + .sendCertificateChain(true) + .authorityHost(getAuthorityHost(azureEnv)) + .httpClient(HttpClientRetriever.get()) + .build(); + } public boolean isPromptAccount() { return promptAccount; } @@ -192,16 +212,24 @@ public String getClientSecretSecret() { return clientSecret.getEncryptedValue(); } + public String getClientCertificateSecret() { + return clientCertificate.getEncryptedValue(); + } + + public String getCredentialType() { + return credentialType; + } public String getTenantSecret() { return tenant.getEncryptedValue(); } String getCredentialCacheKey() { - return Util.getDigestOf(clientId.getPlainText() - + clientSecret.getPlainText() + String credentialComponent = clientId.getPlainText() + + ("Certificate".equals(credentialType) ? clientCertificate.getPlainText() : clientSecret.getPlainText()) + tenant.getPlainText() - + azureEnvironmentName - ); + + azureEnvironmentName; + + return Util.getDigestOf(credentialComponent); } public String getClientId() { @@ -230,6 +258,11 @@ public void setDisableGraphIntegration(boolean disableGraphIntegration) { this.disableGraphIntegration = disableGraphIntegration; } + @DataBoundSetter + public void setCredentialType(String credentialType) { + this.credentialType = credentialType; + } + public void setClientId(String clientId) { this.clientId = Secret.fromString(clientId); } @@ -238,10 +271,19 @@ public Secret getClientSecret() { return clientSecret; } + public Secret getClientCertificate() { + return clientCertificate; + } + public void setClientSecret(String clientSecret) { this.clientSecret = Secret.fromString(clientSecret); } + @DataBoundSetter + public void setClientCertificate(String clientCertificate) { + this.clientCertificate = Secret.fromString(clientCertificate); + } + public String getTenant() { return tenant.getPlainText(); } @@ -277,7 +319,7 @@ public JwtConsumer getJwtConsumer() { OAuth20Service getOAuthService() { return new ServiceBuilder(clientId.getPlainText()) - .apiSecret(clientSecret.getPlainText()) + .apiSecret("Certificate".equals(credentialType) ? clientCertificate.getPlainText() : clientSecret.getPlainText()) .responseType("id_token") .defaultScope("openid profile email") .callback(getRootUrl() + CALLBACK_URL) @@ -619,10 +661,20 @@ public void marshal(Object source, HierarchicalStreamWriter writer, MarshallingC writer.setValue(realm.getClientIdSecret()); writer.endNode(); - writer.startNode(CONVERTER_NODE_CLIENT_SECRET); - writer.setValue(realm.getClientSecretSecret()); + writer.startNode(CONVERTER_NODE_CREDENTIAL_TYPE); + writer.setValue(realm.getCredentialType()); writer.endNode(); + if ("Secret".equals(realm.getCredentialType())) { + writer.startNode(CONVERTER_NODE_CLIENT_SECRET); + writer.setValue(realm.getClientSecretSecret()); + writer.endNode(); + } else { + writer.startNode(CONVERTER_NODE_CLIENT_CERTIFICATE); + writer.setValue(realm.getClientCertificateSecret()); + writer.endNode(); + } + writer.startNode(CONVERTER_NODE_TENANT); writer.setValue(realm.getTenantSecret()); writer.endNode(); @@ -666,6 +718,12 @@ public Object unmarshal(HierarchicalStreamReader reader, UnmarshallingContext co case CONVERTER_NODE_CLIENT_SECRET: realm.setClientSecret(value); break; + case CONVERTER_NODE_CLIENT_CERTIFICATE: + realm.setClientCertificate(value); + break; + case CONVERTER_NODE_CREDENTIAL_TYPE: + realm.setCredentialType(value); + break; case CONVERTER_NODE_TENANT: realm.setTenant(value); break; @@ -746,10 +804,27 @@ public ListBoxModel doFillAzureEnvironmentNameItems() { public FormValidation doVerifyConfiguration(@QueryParameter final String clientId, @QueryParameter final Secret clientSecret, + @QueryParameter final Secret clientCertificate, + @QueryParameter final String credentialType, @QueryParameter final String tenant, @QueryParameter final String testObject, @QueryParameter final String azureEnvironmentName) { - if (testObject.equals("")) { + switch (credentialType) { + case "Secret": + if (Secret.toString(clientSecret).isEmpty()) { + return FormValidation.error("Please set a secret"); + } + break; + case "Certificate": + if (Secret.toString(clientCertificate).isEmpty()) { + return FormValidation.error("Please set a certificate"); + } + break; + default: + return FormValidation.error("Invalid credential type"); + } + + if (testObject.isEmpty()) { return FormValidation.error("Please set a test user principal name or object ID"); } @@ -757,6 +832,8 @@ public FormValidation doVerifyConfiguration(@QueryParameter final String clientI new GraphClientCacheKey( clientId, Secret.toString(clientSecret), + Secret.toString(clientCertificate), + credentialType, tenant, azureEnvironmentName ) diff --git a/src/main/java/com/microsoft/jenkins/azuread/GraphClientCache.java b/src/main/java/com/microsoft/jenkins/azuread/GraphClientCache.java index 8a8aa0ae..71759cfd 100644 --- a/src/main/java/com/microsoft/jenkins/azuread/GraphClientCache.java +++ b/src/main/java/com/microsoft/jenkins/azuread/GraphClientCache.java @@ -1,7 +1,10 @@ package com.microsoft.jenkins.azuread; +import com.azure.core.credential.TokenCredential; import com.azure.identity.ClientSecretCredential; import com.azure.identity.ClientSecretCredentialBuilder; +import com.azure.identity.ClientCertificateCredential; +import com.azure.identity.ClientCertificateCredentialBuilder; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.LoadingCache; import com.microsoft.graph.authentication.TokenCredentialAuthProvider; @@ -20,6 +23,9 @@ import org.apache.commons.lang3.StringUtils; import java.net.Proxy; +import java.io.ByteArrayInputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; import static com.microsoft.jenkins.azuread.AzureEnvironment.AZURE_PUBLIC_CLOUD; import static com.microsoft.jenkins.azuread.AzureEnvironment.getAuthorityHost; @@ -35,14 +41,7 @@ public class GraphClientCache { .build(GraphClientCache::createGraphClient); private static GraphServiceClient<Request> createGraphClient(GraphClientCacheKey key) { - final ClientSecretCredential clientSecretCredential = getClientSecretCredential(key); - - String graphResource = AzureEnvironment.getGraphResource(key.getAzureEnvironmentName()); - - final TokenCredentialAuthProvider authProvider = new TokenCredentialAuthProvider( - singletonList(graphResource + ".default"), - clientSecretCredential - ); + TokenCredentialAuthProvider authProvider = getAuthProvider(key); OkHttpClient.Builder builder = HttpClients.createDefault(authProvider) .newBuilder(); @@ -63,6 +62,33 @@ private static GraphServiceClient<Request> createGraphClient(GraphClientCacheKey return graphServiceClient; } + private static TokenCredentialAuthProvider getAuthProvider(GraphClientCacheKey key) { + String graphResource = AzureEnvironment.getGraphResource(key.getAzureEnvironmentName()); + + TokenCredential tokenCredential; + if ("Secret".equals(key.getCredentialType())) { + tokenCredential = getClientSecretCredential(key); + } else if ("Certificate".equals(key.getCredentialType())) { + tokenCredential = getClientCertificateCredential(key); + } else { + throw new IllegalArgumentException("Invalid credential type"); + } + return new TokenCredentialAuthProvider( + singletonList(graphResource + ".default"), + tokenCredential); + } + + static ClientCertificateCredential getClientCertificateCredential(GraphClientCacheKey key) { + return new ClientCertificateCredentialBuilder() + .clientId(key.getClientId()) + .pemCertificate(getCertificate(key)) + .tenantId(key.getTenantId()) + .sendCertificateChain(true) + .authorityHost(getAuthorityHost(key.getAzureEnvironmentName())) + .httpClient(HttpClientRetriever.get()) + .build(); + } + static ClientSecretCredential getClientSecretCredential(GraphClientCacheKey key) { return new ClientSecretCredentialBuilder() .clientId(key.getClientId()) @@ -73,6 +99,12 @@ static ClientSecretCredential getClientSecretCredential(GraphClientCacheKey key) .build(); } + static InputStream getCertificate(GraphClientCacheKey key) { + + String secretString = key.getClientCertificate(); + return new ByteArrayInputStream(secretString.getBytes(StandardCharsets.UTF_8)); + } + static GraphServiceClient<Request> getClient(GraphClientCacheKey key) { return TOKEN_CACHE.get(key); } @@ -81,6 +113,8 @@ public static GraphServiceClient<Request> getClient(AzureSecurityRealm azureSecu GraphClientCacheKey key = new GraphClientCacheKey( azureSecurityRealm.getClientId(), Secret.toString(azureSecurityRealm.getClientSecret()), + Secret.toString(azureSecurityRealm.getClientCertificate()), + azureSecurityRealm.getCredentialType(), azureSecurityRealm.getTenant(), azureSecurityRealm.getAzureEnvironmentName() ); @@ -111,4 +145,4 @@ public static OkHttpClient.Builder addProxyToHttpClientIfRequired(OkHttpClient.B return builder; } -} +} \ No newline at end of file diff --git a/src/main/java/com/microsoft/jenkins/azuread/GraphClientCacheKey.java b/src/main/java/com/microsoft/jenkins/azuread/GraphClientCacheKey.java index a758d581..b6acd18d 100644 --- a/src/main/java/com/microsoft/jenkins/azuread/GraphClientCacheKey.java +++ b/src/main/java/com/microsoft/jenkins/azuread/GraphClientCacheKey.java @@ -5,12 +5,16 @@ class GraphClientCacheKey { private final String clientId; private final String clientSecret; + private final String clientCertificate; private final String tenantId; private final String azureEnvironmentName; + private final String credentialType; - public GraphClientCacheKey(String clientId, String clientSecret, String tenantId, String azureEnvironmentName) { + public GraphClientCacheKey(String clientId, String clientSecret, String clientCertificate, String credentialType, String tenantId, String azureEnvironmentName) { this.clientId = clientId; this.clientSecret = clientSecret; + this.clientCertificate = clientCertificate; + this.credentialType = credentialType; this.tenantId = tenantId; this.azureEnvironmentName = azureEnvironmentName; } @@ -19,27 +23,35 @@ public GraphClientCacheKey(String clientId, String clientSecret, String tenantId public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; - GraphClientCacheKey cacheKey = (GraphClientCacheKey) o; - return Objects.equals(clientId, cacheKey.clientId) && Objects.equals(clientSecret, cacheKey.clientSecret) && Objects.equals(tenantId, cacheKey.tenantId) && Objects.equals(azureEnvironmentName, cacheKey.azureEnvironmentName); + GraphClientCacheKey that = (GraphClientCacheKey) o; + return Objects.equals(clientId, that.clientId) && + Objects.equals(clientSecret, that.clientSecret) && + Objects.equals(clientCertificate, that.clientCertificate) && + Objects.equals(credentialType, that.credentialType) && + Objects.equals(tenantId, that.tenantId) && + Objects.equals(azureEnvironmentName, that.azureEnvironmentName); } @Override public int hashCode() { - return Objects.hash(clientId, clientSecret, tenantId, azureEnvironmentName); + return Objects.hash(clientId, clientSecret, clientCertificate, credentialType, tenantId, azureEnvironmentName); } public String getClientId() { return clientId; } - public String getClientSecret() { return clientSecret; } - + public String getClientCertificate() { + return clientCertificate; + } + public String getCredentialType() { + return credentialType; + } public String getTenantId() { return tenantId; } - public String getAzureEnvironmentName() { return azureEnvironmentName; } diff --git a/src/main/resources/com/microsoft/jenkins/azuread/AzureSecurityRealm/config.jelly b/src/main/resources/com/microsoft/jenkins/azuread/AzureSecurityRealm/config.jelly index 4f526352..019252ec 100644 --- a/src/main/resources/com/microsoft/jenkins/azuread/AzureSecurityRealm/config.jelly +++ b/src/main/resources/com/microsoft/jenkins/azuread/AzureSecurityRealm/config.jelly @@ -6,15 +6,27 @@ <j:jelly xmlns:j="jelly:core" xmlns:st="jelly:stapler" xmlns:d="jelly:define" xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"> <f:block> - <f:entry title="Client ID" field="clientId" help="/plugin/azure-ad/help/help-client-id.html" > + <f:entry title="Client ID" field="clientId" > <f:textbox /> </f:entry> - <f:entry title="Client Secret" field="clientSecret" help="/plugin/azure-ad/help/help-client-secret.html"> - <f:password /> + <f:entry title="Authentication Type"> + <f:radioBlock name="credentialType" value="Secret" title="Client Secret" help="${null}" + checked="${(instance.credentialType == null) ? 'true' : (instance.credentialType == 'Secret')}" inline="true"> + <f:entry title="Secret" field="clientSecret"> + <f:password/> + </f:entry> + </f:radioBlock> + <f:radioBlock name="credentialType" value="Certificate" title="Client Certificate" + help="${null}" + checked="${instance.credentialType == 'Certificate'}" inline="true"> + <f:entry title="Certificate" field="clientCertificate"> + <f:secretTextarea/> + </f:entry> + </f:radioBlock> </f:entry> - <f:entry title="Tenant" field="tenant" help="/plugin/azure-ad/help/help-tenant.html"> + <f:entry title="Tenant" field="tenant"> <f:textbox /> </f:entry> @@ -22,11 +34,11 @@ <f:select/> </f:entry> - <f:entry title="Cache Duration" field="cacheDuration" help="/plugin/azure-ad/help/help-cache-duration.html"> + <f:entry title="Cache Duration" field="cacheDuration"> <f:number default="3600" /> </f:entry> - <f:entry title="Callback URL from request" field="fromRequest" help="/plugin/azure-ad/help/help-from-request.html"> + <f:entry title="Callback URL from request" field="fromRequest"> <f:checkbox /> </f:entry> @@ -34,6 +46,7 @@ <f:checkbox /> </f:entry> + <f:entry title="${%Enable Single Logout}" field="singleLogout"> <f:checkbox /> </f:entry> @@ -47,7 +60,7 @@ </f:entry> <p>${%Save any configuration changes before configuring authorization settings}</p> - <f:validateButton title="Verify configuration" method="verifyConfiguration" progress="Verifying..." with="clientId,clientSecret,tenant,testObject,azureEnvironmentName"/> + <f:validateButton title="Verify configuration" method="verifyConfiguration" progress="Verifying..." with="clientId,clientSecret,clientCertificate,credentialType,azureEnvironmentName,tenant,testObject"/> </f:block> -</j:jelly> +</j:jelly> \ No newline at end of file diff --git a/src/main/webapp/help/help-cache-duration.html b/src/main/resources/com/microsoft/jenkins/azuread/AzureSecurityRealm/help-cacheDuration.html similarity index 100% rename from src/main/webapp/help/help-cache-duration.html rename to src/main/resources/com/microsoft/jenkins/azuread/AzureSecurityRealm/help-cacheDuration.html diff --git a/src/main/resources/com/microsoft/jenkins/azuread/AzureSecurityRealm/help-clientCertificate.html b/src/main/resources/com/microsoft/jenkins/azuread/AzureSecurityRealm/help-clientCertificate.html new file mode 100644 index 00000000..f307e20f --- /dev/null +++ b/src/main/resources/com/microsoft/jenkins/azuread/AzureSecurityRealm/help-clientCertificate.html @@ -0,0 +1,14 @@ +<div> + Input the private key and the client certificate with which Jenkins will authenticate itself to Microsoft Entra ID. + They must be in PEM format (<a href="https://www.rfc-editor.org/rfc/rfc7468">IETF RFC 7468</a> sections 10 and 5), concatenated together like this: +<pre> +-----BEGIN PRIVATE KEY----- +[Base64 text not shown] +-----END PRIVATE KEY----- +-----BEGIN CERTIFICATE----- +[Base64 text not shown] +-----END CERTIFICATE----- +</pre> + The private key must not be encrypted. + The certificate may be self-signed, but it must not have expired. +</div> \ No newline at end of file diff --git a/src/main/webapp/help/help-client-id.html b/src/main/resources/com/microsoft/jenkins/azuread/AzureSecurityRealm/help-clientId.html similarity index 100% rename from src/main/webapp/help/help-client-id.html rename to src/main/resources/com/microsoft/jenkins/azuread/AzureSecurityRealm/help-clientId.html diff --git a/src/main/webapp/help/help-client-secret.html b/src/main/resources/com/microsoft/jenkins/azuread/AzureSecurityRealm/help-clientSecret.html similarity index 100% rename from src/main/webapp/help/help-client-secret.html rename to src/main/resources/com/microsoft/jenkins/azuread/AzureSecurityRealm/help-clientSecret.html diff --git a/src/main/webapp/help/help-from-request.html b/src/main/resources/com/microsoft/jenkins/azuread/AzureSecurityRealm/help-fromRequest.html similarity index 100% rename from src/main/webapp/help/help-from-request.html rename to src/main/resources/com/microsoft/jenkins/azuread/AzureSecurityRealm/help-fromRequest.html diff --git a/src/main/webapp/help/help-tenant.html b/src/main/resources/com/microsoft/jenkins/azuread/AzureSecurityRealm/help-tenant.html similarity index 100% rename from src/main/webapp/help/help-tenant.html rename to src/main/resources/com/microsoft/jenkins/azuread/AzureSecurityRealm/help-tenant.html diff --git a/src/main/resources/com/microsoft/jenkins/azuread/Messages.properties b/src/main/resources/com/microsoft/jenkins/azuread/Messages.properties index 2a273859..225f84c1 100644 --- a/src/main/resources/com/microsoft/jenkins/azuread/Messages.properties +++ b/src/main/resources/com/microsoft/jenkins/azuread/Messages.properties @@ -8,6 +8,7 @@ Azure_Invalid_SubscriptionId=The subscription id is not valid Azure_SubscriptionID_Missing=Error: Subscription ID is missing. Azure_ClientID_Missing=Error: Client ID is missing. Azure_ClientSecret_Missing=Error: Client Secret is missing. +Azure_ClientCertificate_Missing=Error: Client Certificate is missing. Azure_OAuthToken_Missing=Error: OAuth 2.0 Token Endpoint is missing. Azure_OAuthToken_Malformed=Error: OAuth 2.0 Token Endpoint is malformed. AzureAdLogoutAction_DisplayName=Logged out diff --git a/src/test/java/com/microsoft/jenkins/azuread/AzureAdConfigurationSaveTest.java b/src/test/java/com/microsoft/jenkins/azuread/AzureAdConfigurationSaveTest.java index b8741792..6383e364 100644 --- a/src/test/java/com/microsoft/jenkins/azuread/AzureAdConfigurationSaveTest.java +++ b/src/test/java/com/microsoft/jenkins/azuread/AzureAdConfigurationSaveTest.java @@ -3,25 +3,42 @@ import hudson.util.Secret; import org.junit.Rule; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; import org.jvnet.hudson.test.RestartableJenkinsRule; +import java.util.Arrays; +import java.util.Collection; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.core.Is.is; +@RunWith(Parameterized.class) public class AzureAdConfigurationSaveTest { public static final String TENANT = "tenant"; public static final String CLIENT_ID = "clientId"; public static final String CLIENT_SECRET = "thisIsSpecialSecret"; + public static final String CLIENT_CERTIFICATE = "thisIsSpecialCertificateSecret"; public static final int CACHE_DURATION = 15; + @Rule public final RestartableJenkinsRule r = new RestartableJenkinsRule(); + @Parameterized.Parameter(0) + public String credentialType; + + @Parameterized.Parameters(name = "{index}: credentialType={0}") + public static Collection<Object[]> data() { + return Arrays.asList(new Object[][] { + {"Secret"}, + {"Certificate"} + }); + } + @Test public void FromRequestSaveTest() { - - r.then(r->{ + r.then(r -> { AzureSecurityRealm realm = new AzureSecurityRealm( TENANT, CLIENT_ID, @@ -31,25 +48,34 @@ public void FromRequestSaveTest() { r.jenkins.setSecurityRealm(realm); AzureSecurityRealm result = (AzureSecurityRealm) r.jenkins.getSecurityRealm(); - assertThat(result, is(notNullValue())); - assertThat(result.isFromRequest(), is(true)); - assertThat(result.getTenant(), is(TENANT)); - assertThat(result.getClientId(), is(CLIENT_ID)); - assertThat(result.getClientSecret().getPlainText(), is(CLIENT_SECRET)); - assertThat(result.getCacheDuration(), is(CACHE_DURATION)); - + result.setCredentialType(credentialType); + if ("Certificate".equals(credentialType)) { + result.setClientCertificate(CLIENT_CERTIFICATE); + } + verifyRealm(result, credentialType); }); + r.then(r -> { AzureSecurityRealm result = (AzureSecurityRealm) r.jenkins.getSecurityRealm(); - assertThat(result, is(notNullValue())); - assertThat(result.isFromRequest(), is(true)); - assertThat(result.getTenant(), is(TENANT)); - assertThat(result.getClientId(), is(CLIENT_ID)); - assertThat(result.getClientSecret().getPlainText(), is(CLIENT_SECRET)); - assertThat(result.getCacheDuration(), is(CACHE_DURATION)); - + result.setCredentialType(credentialType); + if ("Certificate".equals(credentialType)) { + result.setClientCertificate(CLIENT_CERTIFICATE); + } + verifyRealm(result, credentialType); }); - } + private void verifyRealm(AzureSecurityRealm realm, String credentialType) { + assertThat(realm, is(notNullValue())); + assertThat(realm.isFromRequest(), is(true)); + assertThat(realm.getTenant(), is(TENANT)); + assertThat(realm.getClientId(), is(CLIENT_ID)); + if ("Secret".equals(credentialType)) { + assertThat(realm.getClientSecret().getPlainText(), is(CLIENT_SECRET)); + } else if ("Certificate".equals(credentialType)) { + assertThat(realm.getClientCertificate().getPlainText(), is(CLIENT_CERTIFICATE)); + } + assertThat(realm.getCredentialType(), is(credentialType)); + assertThat(realm.getCacheDuration(), is(CACHE_DURATION)); + } } diff --git a/src/test/java/com/microsoft/jenkins/azuread/AzureSecurityRealmTest.java b/src/test/java/com/microsoft/jenkins/azuread/AzureSecurityRealmTest.java index 6b50f0f8..08d248c3 100644 --- a/src/test/java/com/microsoft/jenkins/azuread/AzureSecurityRealmTest.java +++ b/src/test/java/com/microsoft/jenkins/azuread/AzureSecurityRealmTest.java @@ -8,17 +8,34 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; import org.jvnet.hudson.test.JenkinsRule; import java.io.ByteArrayInputStream; import java.nio.charset.StandardCharsets; +import java.util.Arrays; +import java.util.Collection; import static org.junit.Assert.assertFalse; +@RunWith(Parameterized.class) public class AzureSecurityRealmTest { @Rule public JenkinsRule j = new JenkinsRule(); + @Parameterized.Parameter(0) + public String credentialType; + + @Parameters(name = "{index}: credentialType={0}") + public static Collection<Object[]> data() { + return Arrays.asList(new Object[][] { + {"Secret"}, + {"Certificate"} + }); + } + @Before public void init() throws Exception { j.recipe(); @@ -29,8 +46,11 @@ public void testConverter() { BinaryStreamWriter writer = null; BinaryStreamReader reader = null; try { - - AzureSecurityRealm securityRealm = new AzureSecurityRealm("tenant", "clientId", Secret.fromString("secret"), 0); + String secret = "secret"; + String certificate = "certificate"; + AzureSecurityRealm securityRealm = new AzureSecurityRealm("tenant", "clientId", Secret.fromString(secret), 0); + securityRealm.setClientCertificate(certificate); + securityRealm.setCredentialType(credentialType); AzureSecurityRealm.ConverterImpl converter = new AzureSecurityRealm.ConverterImpl(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); writer = new BinaryStreamWriter(outputStream); @@ -43,7 +63,11 @@ public void testConverter() { Assert.assertEquals(securityRealm.getTenant(), result.getTenant()); Assert.assertEquals(securityRealm.getClientId(), result.getClientId()); - Assert.assertEquals(securityRealm.getClientSecret().getPlainText(), result.getClientSecret().getPlainText()); + if ("Secret".equals(credentialType)) { + Assert.assertEquals(securityRealm.getClientSecret().getPlainText(), result.getClientSecret().getPlainText()); + } else if ("Certificate".equals(credentialType)) { + Assert.assertEquals(securityRealm.getClientCertificate().getPlainText(), result.getClientCertificate().getPlainText()); + } Assert.assertEquals(securityRealm.getCacheDuration(), result.getCacheDuration()); } finally { if (writer != null) { @@ -60,17 +84,18 @@ public void testSavedConfig() { BinaryStreamWriter writer = null; try { String secretString = "thisIsSpecialSecret"; + String certificateString = "thisIsSpecialCertificate"; AzureSecurityRealm securityRealm = new AzureSecurityRealm("tenant", "clientId", Secret.fromString(secretString), 0); - AzureSecurityRealm.ConverterImpl converter = new AzureSecurityRealm.ConverterImpl(); ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); writer = new BinaryStreamWriter(outputStream); converter.marshal(securityRealm, writer, null); assertFalse(outputStream.toString(StandardCharsets.UTF_8).contains(secretString)); + assertFalse(outputStream.toString(StandardCharsets.UTF_8).contains(certificateString)); } finally { if (writer != null) { writer.close(); } } } -} +} \ No newline at end of file diff --git a/src/test/java/com/microsoft/jenkins/azuread/integrations/casc/ConfigAsCodeTest.java b/src/test/java/com/microsoft/jenkins/azuread/integrations/casc/BaseConfigAsCodeTest.java similarity index 65% rename from src/test/java/com/microsoft/jenkins/azuread/integrations/casc/ConfigAsCodeTest.java rename to src/test/java/com/microsoft/jenkins/azuread/integrations/casc/BaseConfigAsCodeTest.java index ee2e8705..ee70124c 100644 --- a/src/test/java/com/microsoft/jenkins/azuread/integrations/casc/ConfigAsCodeTest.java +++ b/src/test/java/com/microsoft/jenkins/azuread/integrations/casc/BaseConfigAsCodeTest.java @@ -4,30 +4,18 @@ import com.microsoft.jenkins.azuread.AzureAdAuthorizationMatrixFolderProperty; import com.microsoft.jenkins.azuread.AzureAdAuthorizationMatrixNodeProperty; import com.microsoft.jenkins.azuread.AzureAdMatrixAuthorizationStrategy; -import com.microsoft.jenkins.azuread.AzureSecurityRealm; import com.microsoft.jenkins.azuread.PermissionEntry; import hudson.model.Computer; import hudson.model.Item; import hudson.model.Node; import hudson.security.AuthorizationStrategy; -import hudson.security.SecurityRealm; -import io.jenkins.plugins.casc.ConfigurationContext; -import io.jenkins.plugins.casc.ConfiguratorRegistry; -import io.jenkins.plugins.casc.misc.ConfiguredWithCode; import io.jenkins.plugins.casc.misc.JenkinsConfiguredWithCodeRule; -import io.jenkins.plugins.casc.model.CNode; import jenkins.model.Jenkins; import org.jenkinsci.plugins.matrixauth.inheritance.NonInheritingStrategy; -import org.junit.ClassRule; import org.junit.Rule; -import org.junit.Test; import org.jvnet.hudson.test.LoggerRule; - import java.util.logging.Level; -import static io.jenkins.plugins.casc.misc.Util.getJenkinsRoot; -import static io.jenkins.plugins.casc.misc.Util.toStringFromYamlFile; -import static io.jenkins.plugins.casc.misc.Util.toYamlString; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; @@ -35,34 +23,15 @@ import static org.hamcrest.Matchers.notNullValue; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; -public class ConfigAsCodeTest { - private static final String TEST_UPN = "abc@jenkins.com"; - - @ClassRule - @ConfiguredWithCode("configuration-as-code.yml") - public static JenkinsConfiguredWithCodeRule j = new JenkinsConfiguredWithCodeRule(); - +public abstract class BaseConfigAsCodeTest { + protected static final String TEST_UPN = "abc@jenkins.com"; + @Rule - public LoggerRule l = new LoggerRule().record(MatrixAuthorizationStrategyConfigurator.class, Level.WARNING).capture(20); - - @Test - public void should_support_configuration_as_code() { - SecurityRealm securityRealm = j.jenkins.getSecurityRealm(); - assertTrue("security realm", securityRealm instanceof AzureSecurityRealm); - AzureSecurityRealm azureSecurityRealm = (AzureSecurityRealm) securityRealm; - assertNotEquals("clientId", azureSecurityRealm.getClientIdSecret()); - assertNotEquals("clientSecret", azureSecurityRealm.getClientSecretSecret()); - assertNotEquals("tenantId", azureSecurityRealm.getTenantSecret()); - assertEquals("clientId", azureSecurityRealm.getClientId()); - assertEquals("clientSecret", azureSecurityRealm.getClientSecret().getPlainText()); - assertEquals("tenantId", azureSecurityRealm.getTenant()); - assertEquals(0, azureSecurityRealm.getCacheDuration()); - assertTrue(azureSecurityRealm.isFromRequest()); - + public LoggerRule l = new LoggerRule().record(AzureAdMatrixAuthorizationStrategy.class, Level.WARNING).capture(20); + protected void validateCommonAssertions(JenkinsConfiguredWithCodeRule j) { AuthorizationStrategy authorizationStrategy = j.jenkins.getAuthorizationStrategy(); assertTrue("authorization strategy", authorizationStrategy instanceof AzureAdMatrixAuthorizationStrategy); AzureAdMatrixAuthorizationStrategy azureAdMatrixAuthorizationStrategy = (AzureAdMatrixAuthorizationStrategy) authorizationStrategy; @@ -73,7 +42,6 @@ public void should_support_configuration_as_code() { assertTrue("authenticated can build", azureAdMatrixAuthorizationStrategy.hasExplicitPermission(PermissionEntry.group("authenticated"), Item.BUILD)); assertTrue("authenticated can delete jobs", azureAdMatrixAuthorizationStrategy.hasExplicitPermission(PermissionEntry.user(TEST_UPN), Item.DELETE)); assertTrue("authenticated can administer", azureAdMatrixAuthorizationStrategy.hasExplicitPermission(PermissionEntry.user(TEST_UPN), Jenkins.ADMINISTER)); - assertEquals("no warnings", 0, l.getMessages().size()); { @@ -104,22 +72,8 @@ public void should_support_configuration_as_code() { assertFalse(property.hasExplicitPermission(PermissionEntry.user("anonymous"), Item.READ)); assertTrue(property.hasExplicitPermission(PermissionEntry.group(groupSid), Item.CONFIGURE)); assertTrue(property.hasExplicitPermission(PermissionEntry.group(groupSid), Item.DELETE)); - String userSid = "c411116f-cfa6-472c-8ccf-d0cb6053c9aa"; assertTrue(property.hasExplicitPermission(PermissionEntry.user(userSid), Item.BUILD)); } } - - @Test - public void export_configuration() throws Exception { - ConfiguratorRegistry registry = ConfiguratorRegistry.get(); - ConfigurationContext context = new ConfigurationContext(registry); - CNode yourAttribute = getJenkinsRoot(context).get("authorizationStrategy"); - - String exported = toYamlString(yourAttribute); - - String expected = toStringFromYamlFile(this, "configuration-as-code-exported.yml"); - - assertThat(exported, is(expected)); - } -} +} \ No newline at end of file diff --git a/src/test/java/com/microsoft/jenkins/azuread/integrations/casc/ConfigAsCodeClientCertificateTest.java b/src/test/java/com/microsoft/jenkins/azuread/integrations/casc/ConfigAsCodeClientCertificateTest.java new file mode 100644 index 00000000..70a3a517 --- /dev/null +++ b/src/test/java/com/microsoft/jenkins/azuread/integrations/casc/ConfigAsCodeClientCertificateTest.java @@ -0,0 +1,37 @@ +package com.microsoft.jenkins.azuread.integrations.casc; + +import com.microsoft.jenkins.azuread.AzureSecurityRealm; +import hudson.security.SecurityRealm; +import io.jenkins.plugins.casc.misc.ConfiguredWithCode; +import io.jenkins.plugins.casc.misc.JenkinsConfiguredWithCodeRule; +import org.junit.ClassRule; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +public class ConfigAsCodeClientCertificateTest extends BaseConfigAsCodeTest { + + @ClassRule + @ConfiguredWithCode("configuration-as-code-certificate-auth.yml") + public static JenkinsConfiguredWithCodeRule jCertificate = new JenkinsConfiguredWithCodeRule(); + + @Test + public void should_support_configuration_as_code_clientCertificate() { + SecurityRealm securityRealm = jCertificate.jenkins.getSecurityRealm(); + assertTrue("security realm", securityRealm instanceof AzureSecurityRealm); + AzureSecurityRealm azureSecurityRealm = (AzureSecurityRealm) securityRealm; + assertNotEquals("clientId", azureSecurityRealm.getClientIdSecret()); + assertNotEquals("clientCertificate", azureSecurityRealm.getClientCertificateSecret()); + assertNotEquals("tenantId", azureSecurityRealm.getTenantSecret()); + assertEquals("clientId", azureSecurityRealm.getClientId()); + assertEquals("Certificate", azureSecurityRealm.getCredentialType()); + assertEquals("clientCertificate", azureSecurityRealm.getClientCertificate().getPlainText()); + assertEquals("tenantId", azureSecurityRealm.getTenant()); + assertEquals(0, azureSecurityRealm.getCacheDuration()); + assertTrue(azureSecurityRealm.isFromRequest()); + + validateCommonAssertions(jCertificate); + } +} diff --git a/src/test/java/com/microsoft/jenkins/azuread/integrations/casc/ConfigAsCodeClientSecretTest.java b/src/test/java/com/microsoft/jenkins/azuread/integrations/casc/ConfigAsCodeClientSecretTest.java new file mode 100644 index 00000000..4c755769 --- /dev/null +++ b/src/test/java/com/microsoft/jenkins/azuread/integrations/casc/ConfigAsCodeClientSecretTest.java @@ -0,0 +1,37 @@ +package com.microsoft.jenkins.azuread.integrations.casc; + +import com.microsoft.jenkins.azuread.AzureSecurityRealm; +import hudson.security.SecurityRealm; +import io.jenkins.plugins.casc.misc.ConfiguredWithCode; +import io.jenkins.plugins.casc.misc.JenkinsConfiguredWithCodeRule; +import org.junit.ClassRule; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertTrue; + +public class ConfigAsCodeClientSecretTest extends BaseConfigAsCodeTest { + + @ClassRule + @ConfiguredWithCode("configuration-as-code-secret-auth.yml") + public static JenkinsConfiguredWithCodeRule jSecret = new JenkinsConfiguredWithCodeRule(); + + @Test + public void should_support_configuration_as_code_clientSecret() { + SecurityRealm securityRealm = jSecret.jenkins.getSecurityRealm(); + assertTrue("security realm", securityRealm instanceof AzureSecurityRealm); + AzureSecurityRealm azureSecurityRealm = (AzureSecurityRealm) securityRealm; + assertNotEquals("clientId", azureSecurityRealm.getClientIdSecret()); + assertNotEquals("clientSecret", azureSecurityRealm.getClientSecretSecret()); + assertNotEquals("tenantId", azureSecurityRealm.getTenantSecret()); + assertEquals("clientId", azureSecurityRealm.getClientId()); + assertEquals("Secret", azureSecurityRealm.getCredentialType()); + assertEquals("clientSecret", azureSecurityRealm.getClientSecret().getPlainText()); + assertEquals("tenantId", azureSecurityRealm.getTenant()); + assertEquals(0, azureSecurityRealm.getCacheDuration()); + assertTrue(azureSecurityRealm.isFromRequest()); + + validateCommonAssertions(jSecret); + } +} diff --git a/src/test/java/com/microsoft/jenkins/azuread/integrations/casc/ConfigAsCodeExportConfigurationTest.java b/src/test/java/com/microsoft/jenkins/azuread/integrations/casc/ConfigAsCodeExportConfigurationTest.java new file mode 100644 index 00000000..f64a5073 --- /dev/null +++ b/src/test/java/com/microsoft/jenkins/azuread/integrations/casc/ConfigAsCodeExportConfigurationTest.java @@ -0,0 +1,32 @@ +package com.microsoft.jenkins.azuread.integrations.casc; + +import io.jenkins.plugins.casc.ConfigurationContext; +import io.jenkins.plugins.casc.ConfiguratorRegistry; +import io.jenkins.plugins.casc.misc.ConfiguredWithCode; +import io.jenkins.plugins.casc.misc.JenkinsConfiguredWithCodeRule; +import io.jenkins.plugins.casc.model.CNode; +import org.junit.ClassRule; +import org.junit.Test; + +import static io.jenkins.plugins.casc.misc.Util.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.CoreMatchers.is; + +public class ConfigAsCodeExportConfigurationTest extends BaseConfigAsCodeTest { + + @ClassRule + @ConfiguredWithCode("configuration-as-code-secret-auth.yml") + public static JenkinsConfiguredWithCodeRule jSecret = new JenkinsConfiguredWithCodeRule(); + + @Test + public void export_configuration() throws Exception { + ConfiguratorRegistry registry = ConfiguratorRegistry.get(); + ConfigurationContext context = new ConfigurationContext(registry); + CNode yourAttribute = getJenkinsRoot(context).get("authorizationStrategy"); + + String exported = toYamlString(yourAttribute); + String expected = toStringFromYamlFile(this, "configuration-as-code-exported.yml"); + assertThat(exported, is(expected)); + } + +} diff --git a/src/test/resources/com/microsoft/jenkins/azuread/integrations/casc/ConfigAsCodeTest/config.xml b/src/test/resources/com/microsoft/jenkins/azuread/integrations/casc/ConfigAsCodeTest/config.xml index aa7cf28e..19024b88 100644 --- a/src/test/resources/com/microsoft/jenkins/azuread/integrations/casc/ConfigAsCodeTest/config.xml +++ b/src/test/resources/com/microsoft/jenkins/azuread/integrations/casc/ConfigAsCodeTest/config.xml @@ -28,6 +28,8 @@ <securityRealm class="com.microsoft.jenkins.azuread.AzureSecurityRealm"> <clientid>clientId</clientid> <clientsecret>password</clientsecret> + <clientcertificate></clientcertificate> + <credentialtype>secret</credentialtype> <tenant>tenantId</tenant> <cacheduration>0</cacheduration> <fromrequest>true</fromrequest> diff --git a/src/test/resources/com/microsoft/jenkins/azuread/integrations/casc/configuration-as-code-certificate-auth.yml b/src/test/resources/com/microsoft/jenkins/azuread/integrations/casc/configuration-as-code-certificate-auth.yml new file mode 100644 index 00000000..afb3b43d --- /dev/null +++ b/src/test/resources/com/microsoft/jenkins/azuread/integrations/casc/configuration-as-code-certificate-auth.yml @@ -0,0 +1,97 @@ +jenkins: + authorizationStrategy: + azureAdMatrix: + entries: + - group: + name: authenticated + permissions: + - Agent/Build + - Job/Build + - user: + name: abc (abc@jenkins.com) + permissions: + - Agent/Configure + - Agent/Connect + - Agent/Create + - Agent/Delete + - Agent/Disconnect + - Credentials/Create + - Credentials/Delete + - Credentials/ManageDomains + - Credentials/Update + - Credentials/View + - Job/Cancel + - Job/Configure + - Job/Create + - Job/Delete + - Job/Discover + - Job/Move + - Job/Read + - Job/Workspace + - Overall/Administer + - Overall/Read + - Run/Delete + - Run/Replay + - Run/Update + - View/Configure + - View/Create + - View/Delete + - View/Read + - user: + name: anonymous + permissions: + - Overall/Read + securityRealm: + azure: + clientid: "clientId" + clientcertificate: "clientCertificate" + credentialtype: "Certificate" + tenant: "tenantId" + cacheduration: 0 + fromrequest: true + nodes: + - permanent: + labelString: "agent" + launcher: + jnlp: + webSocket: true + workDirSettings: + disabled: false + failIfWorkDirIsMissing: false + internalDir: "remoting" + name: "agent" + nodeProperties: + - azureAdAuthorizationMatrix: + inheritanceStrategy: "nonInheriting" + entries: + - user: + name: Adele Vance (be674052-e519-4231-b5e7-2b390bff6346) + permissions: + - "Agent/Build" + - user: + name: Lee Gu (7678bed6-0e7f-4a83-86d2-81d8e47614ee) + permissions: + - "Agent/Disconnect" + remoteFS: "/opt/jenkins" + retentionStrategy: "always" +jobs: + - script: > + folder('generated') { + properties { + azureAdAuthorizationMatrix { + inheritanceStrategy { + nonInheriting() + } + entries { + group { + name('Some group (7fe913e8-6c9f-40f8-913e-7178b7768cc5)') + permissions([ 'Job/Build', 'Job/Configure', 'Job/Delete', 'Job/Read' ]) + } + user { + name('c411116f-cfa6-472c-8ccf-d0cb6053c9aa') + permissions([ 'Job/Build', 'Job/Configure' ]) + } + } + } + } + } diff --git a/src/test/resources/com/microsoft/jenkins/azuread/integrations/casc/configuration-as-code.yml b/src/test/resources/com/microsoft/jenkins/azuread/integrations/casc/configuration-as-code-secret-auth.yml similarity index 100% rename from src/test/resources/com/microsoft/jenkins/azuread/integrations/casc/configuration-as-code.yml rename to src/test/resources/com/microsoft/jenkins/azuread/integrations/casc/configuration-as-code-secret-auth.yml