From 01c414e20d11100e5fd8ade80528587fed22207c Mon Sep 17 00:00:00 2001
From: Martin Lowe <martin.lowe@eclipse-foundation.org>
Date: Wed, 14 Dec 2022 11:45:24 -0500
Subject: [PATCH] Remove OIDC connection and replace with a property-defined
 key

OIDC requests are consistently having proxy/gateway errors, so we are
replacing the solution in the short term. This key will be set in secret
properties and will enable secure enough transmission without having to
block on figuring out NGINX issues.
---
 pom.xml                                       |  19 ----
 .../git/eca/resource/ReportsResource.java     |  19 +---
 src/main/resources/application.properties     |   6 +-
 .../git/eca/resource/ReportsResourceTest.java | 105 ++++++------------
 src/test/resources/application.properties     |   5 +-
 5 files changed, 44 insertions(+), 110 deletions(-)

diff --git a/pom.xml b/pom.xml
index 660a9a93..216db0fa 100644
--- a/pom.xml
+++ b/pom.xml
@@ -61,10 +61,6 @@
       <groupId>io.quarkus</groupId>
       <artifactId>quarkus-resteasy-jackson</artifactId>
     </dependency>
-    <dependency>
-      <groupId>io.quarkus</groupId>
-      <artifactId>quarkus-oidc</artifactId>
-    </dependency>
     <dependency>
       <groupId>io.quarkus</groupId>
       <artifactId>quarkus-smallrye-context-propagation</artifactId>
@@ -131,21 +127,6 @@
       <artifactId>rest-assured</artifactId>
       <scope>test</scope>
     </dependency>
-    <dependency>
-      <groupId>io.quarkus</groupId>
-      <artifactId>quarkus-test-security</artifactId>
-      <scope>test</scope>
-    </dependency>
-    <dependency>
-      <groupId>io.quarkus</groupId>
-      <artifactId>quarkus-test-security-oidc</artifactId>
-      <scope>test</scope>
-    </dependency>
-    <dependency>
-      <groupId>io.quarkus</groupId>
-      <artifactId>quarkus-test-oidc-server</artifactId>
-      <scope>test</scope>
-    </dependency>
     <dependency>
       <groupId>io.rest-assured</groupId>
       <artifactId>json-schema-validator</artifactId>
diff --git a/src/main/java/org/eclipsefoundation/git/eca/resource/ReportsResource.java b/src/main/java/org/eclipsefoundation/git/eca/resource/ReportsResource.java
index 5959731f..284dce18 100644
--- a/src/main/java/org/eclipsefoundation/git/eca/resource/ReportsResource.java
+++ b/src/main/java/org/eclipsefoundation/git/eca/resource/ReportsResource.java
@@ -12,7 +12,6 @@
 package org.eclipsefoundation.git.eca.resource;
 
 import java.time.LocalDate;
-import java.util.List;
 
 import javax.inject.Inject;
 import javax.ws.rs.BadRequestException;
@@ -29,30 +28,24 @@ import org.eclipsefoundation.git.eca.service.ReportsService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import io.quarkus.security.Authenticated;
-import io.quarkus.security.identity.SecurityIdentity;
-
-@Authenticated
 @Path("/reports")
 public class ReportsResource {
     private static final Logger LOGGER = LoggerFactory.getLogger(ReportsResource.class);
 
-    @ConfigProperty(name = "eclipse.reports.allowed-users")
-    List<String> allowedUsers;
+    @ConfigProperty(name = "eclipse.reports.access-key")
+    String key;
 
     @Inject
     RequestWrapper wrap;
     @Inject
     ReportsService reportsService;
-    @Inject
-    SecurityIdentity ident;
 
     @GET
     @Path("/gitlab/private-projects")
-    public Response getPrivateProjectEvents(@QueryParam("status") String status, @QueryParam("since") LocalDate since,
-            @QueryParam("until") LocalDate until) {
-        if (!allowedUsers.contains(ident.getPrincipal().getName())) {
-            LOGGER.debug("User '{}' does not have access to the reports, access blocked", ident.getPrincipal().getName());
+    public Response getPrivateProjectEvents(@QueryParam("key") String passedKey, @QueryParam("status") String status,
+            @QueryParam("since") LocalDate since, @QueryParam("until") LocalDate until) {
+        if (!key.equals(passedKey)) {
+            LOGGER.debug("Bad key passed for access, access blocked");
             return Response.status(401).build();
         }
         if (StringUtils.isNotBlank(status) && !isValidStatus(status)) {
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
index c5c46d3a..8fc5aa32 100644
--- a/src/main/resources/application.properties
+++ b/src/main/resources/application.properties
@@ -25,11 +25,7 @@ quarkus.hibernate-orm.datasource=<default>
 
 ## OAUTH CONFIG
 quarkus.oauth2.enabled=false
-quarkus.oidc.application-type=web-app
-quarkus.oidc.token.refresh-expired=true
-quarkus.oidc.authentication.session-age-extension=60m
-quarkus.oidc.discovery-enabled=true
-quarkus.oidc.roles.source=accesstoken
+quarkus.oidc.enabled=false
 oauth2.scope=eclipsefdn_view_all_profiles
 oauth2.client-id=placeholder
 oauth2.client-secret=placeholder
diff --git a/src/test/java/org/eclipsefoundation/git/eca/resource/ReportsResourceTest.java b/src/test/java/org/eclipsefoundation/git/eca/resource/ReportsResourceTest.java
index cd3983dc..e8be810f 100644
--- a/src/test/java/org/eclipsefoundation/git/eca/resource/ReportsResourceTest.java
+++ b/src/test/java/org/eclipsefoundation/git/eca/resource/ReportsResourceTest.java
@@ -11,144 +11,127 @@
 **********************************************************************/
 package org.eclipsefoundation.git.eca.resource;
 
+import java.util.Optional;
+
 import org.eclipsefoundation.git.eca.namespace.GitEcaParameterNames;
 import org.eclipsefoundation.git.eca.test.namespaces.SchemaNamespaceHelper;
-import org.eclipsefoundation.testing.helpers.AuthHelper;
 import org.eclipsefoundation.testing.helpers.TestCaseHelper;
 import org.eclipsefoundation.testing.templates.RestAssuredTemplates;
 import org.eclipsefoundation.testing.templates.RestAssuredTemplates.EndpointTestCase;
 import org.junit.jupiter.api.Test;
 
 import io.quarkus.test.junit.QuarkusTest;
-import io.quarkus.test.security.TestSecurity;
-import io.quarkus.test.security.oidc.Claim;
-import io.quarkus.test.security.oidc.ConfigMetadata;
-import io.quarkus.test.security.oidc.OidcSecurity;
 import io.restassured.http.ContentType;
 
 @QuarkusTest
 class ReportsResourceTest {
     public static final String REPORTS_BASE_URL = "/reports";
-    public static final String REPORTS_PROJECTS_URL = REPORTS_BASE_URL + "/gitlab/private-projects";
-    public static final String REPORTS_PROJECTS_STATUS_URL = REPORTS_PROJECTS_URL + "?status={status}";
-    public static final String REPORTS_PROJECTS_UNTIL_URL = REPORTS_PROJECTS_URL + "?until={date}";
-    public static final String REPORTS_PROJECTS_SINCE_URL = REPORTS_PROJECTS_URL + "?since={date}";
-    public static final String REPORTS_PROJECTS_RANGE_URL = REPORTS_PROJECTS_URL + "?since={start}&until={end}";
+    public static final String REPORTS_PROJECTS_URL = REPORTS_BASE_URL + "/gitlab/private-projects?key={key}";
+    public static final String REPORTS_PROJECTS_STATUS_URL = REPORTS_PROJECTS_URL + "&status={status}";
+    public static final String REPORTS_PROJECTS_UNTIL_URL = REPORTS_PROJECTS_URL + "&until={date}";
+    public static final String REPORTS_PROJECTS_SINCE_URL = REPORTS_PROJECTS_URL + "&since={date}";
+    public static final String REPORTS_PROJECTS_RANGE_URL = REPORTS_PROJECTS_URL + "&since={start}&until={end}";
+    public static final String VALID_TEST_ACCESS_KEY = "samplekey";
 
     public static final EndpointTestCase GET_REPORT_SUCCESS_CASE = TestCaseHelper
-            .buildSuccessCase(REPORTS_PROJECTS_URL, new String[] {}, SchemaNamespaceHelper.PRIVATE_PROJECT_EVENTS_SCHEMA_PATH);
+            .buildSuccessCase(REPORTS_PROJECTS_URL, new String[] { VALID_TEST_ACCESS_KEY },
+                    SchemaNamespaceHelper.PRIVATE_PROJECT_EVENTS_SCHEMA_PATH);
 
     public static final EndpointTestCase GET_REPORT_ACTIVE_SUCCESS_CASE = TestCaseHelper
-            .buildSuccessCase(REPORTS_PROJECTS_STATUS_URL, new String[] { GitEcaParameterNames.STATUS_ACTIVE.getName() },
+            .buildSuccessCase(REPORTS_PROJECTS_STATUS_URL,
+                    new String[] { VALID_TEST_ACCESS_KEY, GitEcaParameterNames.STATUS_ACTIVE.getName() },
                     SchemaNamespaceHelper.PRIVATE_PROJECT_EVENTS_SCHEMA_PATH);
 
     public static final EndpointTestCase GET_REPORT_DELETED_SUCCESS_CASE = TestCaseHelper
-            .buildSuccessCase(REPORTS_PROJECTS_STATUS_URL, new String[] { GitEcaParameterNames.STATUS_DELETED.getName() },
+            .buildSuccessCase(REPORTS_PROJECTS_STATUS_URL,
+                    new String[] { VALID_TEST_ACCESS_KEY, GitEcaParameterNames.STATUS_DELETED.getName() },
                     SchemaNamespaceHelper.PRIVATE_PROJECT_EVENTS_SCHEMA_PATH);
 
     public static final EndpointTestCase GET_REPORT_SINCE_SUCCESS_CASE = TestCaseHelper
-            .buildSuccessCase(REPORTS_PROJECTS_SINCE_URL, new String[] { "2022-11-11" },
+            .buildSuccessCase(REPORTS_PROJECTS_SINCE_URL, new String[] { VALID_TEST_ACCESS_KEY, "2022-11-11" },
                     SchemaNamespaceHelper.PRIVATE_PROJECT_EVENTS_SCHEMA_PATH);
 
     public static final EndpointTestCase GET_REPORT_UNTIL_SUCCESS_CASE = TestCaseHelper
-            .buildSuccessCase(REPORTS_PROJECTS_UNTIL_URL, new String[] { "2022-11-15" },
+            .buildSuccessCase(REPORTS_PROJECTS_UNTIL_URL, new String[] { VALID_TEST_ACCESS_KEY, "2022-11-15" },
                     SchemaNamespaceHelper.PRIVATE_PROJECT_EVENTS_SCHEMA_PATH);
 
     public static final EndpointTestCase GET_REPORT_RANGE_SUCCESS_CASE = TestCaseHelper
-            .buildSuccessCase(REPORTS_PROJECTS_RANGE_URL, new String[] { "2022-11-15", "2022-11-15" },
+            .buildSuccessCase(REPORTS_PROJECTS_RANGE_URL, new String[] { VALID_TEST_ACCESS_KEY, "2022-11-15", "2022-11-15" },
                     SchemaNamespaceHelper.PRIVATE_PROJECT_EVENTS_SCHEMA_PATH);
 
+    public static final EndpointTestCase GET_REPORT_BAD_ACCESS_KEY = EndpointTestCase
+            .builder()
+            .setPath(REPORTS_PROJECTS_URL)
+            .setStatusCode(401)
+            .setParams(Optional.of(new String[] { "incorrect-key" }))
+            .build();
+
     public static final EndpointTestCase GET_REPORT_BAD_STATUS_CASE = TestCaseHelper
-            .buildBadRequestCase(REPORTS_PROJECTS_STATUS_URL, new String[] { "nope" }, SchemaNamespaceHelper.ERROR_SCHEMA_PATH);
+            .buildBadRequestCase(REPORTS_PROJECTS_STATUS_URL, new String[] { VALID_TEST_ACCESS_KEY, "nope" },
+                    SchemaNamespaceHelper.ERROR_SCHEMA_PATH);
 
     public static final EndpointTestCase GET_REPORT_BAD_UNTIL_CASE = TestCaseHelper
-            .buildBadRequestCase(REPORTS_PROJECTS_UNTIL_URL, new String[] { "nope" }, SchemaNamespaceHelper.ERROR_SCHEMA_PATH);
+            .buildBadRequestCase(REPORTS_PROJECTS_UNTIL_URL, new String[] { VALID_TEST_ACCESS_KEY, "nope" },
+                    SchemaNamespaceHelper.ERROR_SCHEMA_PATH);
 
     public static final EndpointTestCase GET_REPORT_BAD_SINCE_CASE = TestCaseHelper
-            .buildBadRequestCase(REPORTS_PROJECTS_SINCE_URL, new String[] { "nope" }, SchemaNamespaceHelper.ERROR_SCHEMA_PATH);
-
-    public static final EndpointTestCase GET_REPORT_UNAUTHORIZED_CASE = EndpointTestCase
-            .builder()
-            .setPath(REPORTS_PROJECTS_URL)
-            .setStatusCode(401)
-            .build();
+            .buildBadRequestCase(REPORTS_PROJECTS_SINCE_URL, new String[] { VALID_TEST_ACCESS_KEY, "nope" },
+                    SchemaNamespaceHelper.ERROR_SCHEMA_PATH);
 
     /*
      * GET /reports/gitlab/private-projects
      */
     @Test
-    @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE)
-    @OidcSecurity(claims = { @Claim(key = AuthHelper.EMAIL_CLAIM_KEY, value = AuthHelper.EMAIL_CLAIM_VALUE) }, userinfo = {}, config = {
-            @ConfigMetadata(key = AuthHelper.ISSUER_FIELD_KEY, value = AuthHelper.ISSUER_FIELD_VALUE) })
     void getPrivProjReport_success() {
         RestAssuredTemplates.testGet(GET_REPORT_SUCCESS_CASE);
     }
 
     @Test
-    @TestSecurity(user = "jwick", roles = AuthHelper.DEFAULT_ROLE)
-    void getPrivProjReport_success_multipleValidUsers() {
-        RestAssuredTemplates.testGet(GET_REPORT_SUCCESS_CASE);
-    }
-
-    @Test
-    @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE)
     void getPrivProjReport_success_validateSchema() {
         RestAssuredTemplates.testGet_validateSchema(GET_REPORT_SUCCESS_CASE);
     }
 
     @Test
-    @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE)
     void getPrivProjReport_success_validateResponseFormat() {
         RestAssuredTemplates.testGet_validateResponseFormat(GET_REPORT_SUCCESS_CASE);
     }
 
     @Test
-    void getPrivProjReport_failure_unauthenticated() {
-        // Quarkus will apply 401 for failed authentication
-        RestAssuredTemplates.testGet(GET_REPORT_UNAUTHORIZED_CASE);
-    }
-
-    @Test
-    @TestSecurity(user = "badActorz", roles = AuthHelper.DEFAULT_ROLE)
-    void getPrivProjReport_failure_unauthorizedUser() {
-        RestAssuredTemplates.testGet(GET_REPORT_UNAUTHORIZED_CASE);
+    void getPrivProjReport_failure_invalidRequestFormat() {
+        RestAssuredTemplates
+                .testGet(TestCaseHelper
+                        .buildInvalidFormatCase(REPORTS_PROJECTS_URL, new String[] { VALID_TEST_ACCESS_KEY }, ContentType.TEXT));
     }
 
     @Test
-    @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE)
-    void getPrivProjReport_failure_invalidRequestFormat() {
-        RestAssuredTemplates.testGet(TestCaseHelper.buildInvalidFormatCase(REPORTS_PROJECTS_URL, new String[] {}, ContentType.TEXT));
+    void getPrivProjReport_failure_badAccessKey() {
+        RestAssuredTemplates.testGet(GET_REPORT_BAD_ACCESS_KEY);
     }
 
     /*
      * GET /reports/gitlab/private-projects?status=active
      */
     @Test
-    @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE)
     void getPrivProjReportActive_success() {
         RestAssuredTemplates.testGet(GET_REPORT_ACTIVE_SUCCESS_CASE);
     }
 
     @Test
-    @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE)
     void getPrivProjReportActive_success_validateSchema() {
         RestAssuredTemplates.testGet_validateSchema(GET_REPORT_ACTIVE_SUCCESS_CASE);
     }
 
     @Test
-    @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE)
     void getPrivProjReportActive_success_validateResponseFormat() {
         RestAssuredTemplates.testGet_validateResponseFormat(GET_REPORT_ACTIVE_SUCCESS_CASE);
     }
 
     @Test
-    @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE)
     void getPrivProjReportStatus_failure_invalidStatus() {
         RestAssuredTemplates.testGet(GET_REPORT_BAD_STATUS_CASE);
     }
 
     @Test
-    @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE)
     void getPrivProjReportStatus_failure_invalidStatus_validate_schema() {
         RestAssuredTemplates.testGet_validateSchema(GET_REPORT_BAD_STATUS_CASE);
     }
@@ -157,19 +140,16 @@ class ReportsResourceTest {
      * GET /reports/gitlab/private-projects?status=deleted
      */
     @Test
-    @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE)
     void getPrivProjReportDeleted_success() {
         RestAssuredTemplates.testGet(GET_REPORT_DELETED_SUCCESS_CASE);
     }
 
     @Test
-    @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE)
     void getPrivProjReportDeleted_success_validateSchema() {
         RestAssuredTemplates.testGet_validateSchema(GET_REPORT_DELETED_SUCCESS_CASE);
     }
 
     @Test
-    @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE)
     void getPrivProjReportDeleted_success_validateResponseFormat() {
         RestAssuredTemplates.testGet_validateResponseFormat(GET_REPORT_DELETED_SUCCESS_CASE);
     }
@@ -178,31 +158,26 @@ class ReportsResourceTest {
      * GET /reports/webhooks/gitlab/system?since={date}
      */
     @Test
-    @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE)
     void getPrivProjReportSince_success() {
         RestAssuredTemplates.testGet(GET_REPORT_SINCE_SUCCESS_CASE);
     }
 
     @Test
-    @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE)
     void getPrivProjReportSince_success_validateSchema() {
         RestAssuredTemplates.testGet_validateSchema(GET_REPORT_SINCE_SUCCESS_CASE);
     }
 
     @Test
-    @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE)
     void getPrivProjReportSince_success_validateResponseFormat() {
         RestAssuredTemplates.testGet_validateResponseFormat(GET_REPORT_SINCE_SUCCESS_CASE);
     }
 
     @Test
-    @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE)
     void getPrivProjReportSince_failure_invalidStatus() {
         RestAssuredTemplates.testGet(GET_REPORT_BAD_SINCE_CASE);
     }
 
     @Test
-    @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE)
     void getPrivProjReportSince_failure_invalidStatus_validate_schema() {
         RestAssuredTemplates.testGet_validateSchema(GET_REPORT_BAD_SINCE_CASE);
     }
@@ -211,31 +186,26 @@ class ReportsResourceTest {
      * GET /reports/gitlab/private-projects?until={date}
      */
     @Test
-    @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE)
     void getPrivProjReportUntil_success() {
         RestAssuredTemplates.testGet(GET_REPORT_UNTIL_SUCCESS_CASE);
     }
 
     @Test
-    @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE)
     void getPrivProjReportUntil_success_validateSchema() {
         RestAssuredTemplates.testGet_validateSchema(GET_REPORT_UNTIL_SUCCESS_CASE);
     }
 
     @Test
-    @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE)
     void getPrivProjReportUntil_success_validateResponseFormat() {
         RestAssuredTemplates.testGet_validateResponseFormat(GET_REPORT_UNTIL_SUCCESS_CASE);
     }
 
     @Test
-    @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE)
     void getPrivProjReportUntil_failure_invalidStatus() {
         RestAssuredTemplates.testGet(GET_REPORT_BAD_UNTIL_CASE);
     }
 
     @Test
-    @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE)
     void getPrivProjReportSUntil_failure_invalidStatus_validate_schema() {
         RestAssuredTemplates.testGet_validateSchema(GET_REPORT_BAD_UNTIL_CASE);
     }
@@ -244,19 +214,16 @@ class ReportsResourceTest {
      * GET /reports/webhooks/gitlab/system?since={date}&until={date}
      */
     @Test
-    @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE)
     void getPrivProjReportRange_success() {
         RestAssuredTemplates.testGet(GET_REPORT_RANGE_SUCCESS_CASE);
     }
 
     @Test
-    @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE)
     void getPrivProjReportRange_success_validateSchema() {
         RestAssuredTemplates.testGet_validateSchema(GET_REPORT_RANGE_SUCCESS_CASE);
     }
 
     @Test
-    @TestSecurity(user = AuthHelper.TEST_USER_NAME, roles = AuthHelper.DEFAULT_ROLE)
     void getPrivProjReportRange_success_validateResponseFormat() {
         RestAssuredTemplates.testGet_validateResponseFormat(GET_REPORT_RANGE_SUCCESS_CASE);
     }
diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties
index 71bfbed2..78bcff0f 100644
--- a/src/test/resources/application.properties
+++ b/src/test/resources/application.properties
@@ -4,7 +4,7 @@ org.eclipsefoundation.git.eca.api.BotsAPI/mp-rest/url=https://api.eclipse.org
 
 eclipse.noreply.email-patterns=@users.noreply.github.com\$
 eclipse.mail.allowlist=noreply@github.com
-eclipse.reports.allowed-users=opearson,jwick
+eclipse.reports.access-key=samplekey
 eclipse.gitlab.access-token=token_val
 
 ## DATASOURCE CONFIG
@@ -25,6 +25,3 @@ quarkus.http.port=8080
 quarkus.oidc.enabled=false
 quarkus.keycloak.devservices.enabled=false
 quarkus.oidc-client.enabled=false
-quarkus.oidc.client-id=quarkus-service-app
-quarkus.oidc.application-type=service
-
-- 
GitLab