Repository: devopshq/artifactory

Issue:
Support access tokens for authentication
Artifactory Projects requires a valid token to authenticate.

https://www.jfrog.com/confluence/display/JFROG/Artifactory+REST+API#ArtifactoryRESTAPI-PROJECTS
https://www.jfrog.com/confluence/display/JFROG/Access+Tokens


Discussion:

Patch:
diff --git a/README.md b/README.md
index 600b44a..9b21baa 100644
--- a/README.md
+++ b/README.md
@@ -73,9 +73,12 @@ pip install dohq-artifactory==0.5.dev243
 # Usage
 
 ## Authentication ##
+
 `dohq-artifactory` supports these ways of authentication:
-- Username and password (or [API KEY](https://www.jfrog.com/confluence/display/RTF/Updating+Your+Profile#UpdatingYourProfile-APIKey)) to access restricted resources, you can pass ```auth``` parameter to ArtifactoryPath.
+
+- Username and password (or [API KEY](https://www.jfrog.com/confluence/display/RTF/Updating+Your+Profile#UpdatingYourProfile-APIKey), or [Access Tokens](https://www.jfrog.com/confluence/display/JFROG/Access+Tokens#AccessTokens-UsingTokens)) to access restricted resources, you can pass ```auth``` parameter to ArtifactoryPath.
 - [API KEY](https://www.jfrog.com/confluence/display/RTF/Updating+Your+Profile#UpdatingYourProfile-APIKey) can pass with `apikey` parameter.
+- [Access Token](https://www.jfrog.com/confluence/display/JFROG/Access+Tokens#AccessTokens-UsingTokens) can pass with `token` parameter.
 
 ```python
 from artifactory import ArtifactoryPath
@@ -85,10 +88,15 @@ path = ArtifactoryPath(
     "http://my-artifactory/artifactory/myrepo/restricted-path", apikey="MY_API_KEY"
 )
 
+# Access Token
+path = ArtifactoryPath(
+    "http://my-artifactory/artifactory/myrepo/restricted-path", token="MY_ACCESS_TOKEN"
+)
+
 # User and password OR API_KEY
 path = ArtifactoryPath(
     "http://my-artifactory/artifactory/myrepo/restricted-path",
-    auth=("USERNAME", "PASSWORD or API_KEY"),
+    auth=("USERNAME", "PASSWORD, API_KEY or ACCESS_TOKEN"),
 )
 
 # Other authentication types
diff --git a/artifactory.py b/artifactory.py
index 6437d37..bbf9dec 100755
--- a/artifactory.py
+++ b/artifactory.py
@@ -48,6 +48,7 @@ from dohq_artifactory.admin import RepositoryRemote
 from dohq_artifactory.admin import RepositoryVirtual
 from dohq_artifactory.admin import User
 from dohq_artifactory.auth import XJFrogArtApiAuth
+from dohq_artifactory.auth import XJFrogArtBearerAuth
 from dohq_artifactory.exception import ArtifactoryException
 
 try:
@@ -376,8 +377,16 @@ class nullcontext:
 
 def quote_url(url):
     parsed_url = urllib3.util.parse_url(url)
-    quoted_path = requests.utils.quote(url.rpartition(parsed_url.host)[2])
-    quoted_url = "{}://{}{}".format(parsed_url.scheme, parsed_url.host, quoted_path)
+    if parsed_url.port:
+        quoted_path = requests.utils.quote(
+            url.rpartition(f"{parsed_url.host}:{parsed_url.port}")[2]
+        )
+        quoted_url = (
+            f"{parsed_url.scheme}://{parsed_url.host}:{parsed_url.port}{quoted_path}"
+        )
+    else:
+        quoted_path = requests.utils.quote(url.rpartition(parsed_url.host)[2])
+        quoted_url = "{}://{}{}".format(parsed_url.scheme, parsed_url.host, quoted_path)
 
     return quoted_url
 
@@ -1248,13 +1257,18 @@ class ArtifactoryPath(pathlib.Path, PureArtifactoryPath):
 
         # Auth section
         apikey = kwargs.get("apikey")
+        token = kwargs.get("token")
         auth_type = kwargs.get("auth_type")
-        if apikey is None:
+
+        if apikey:
+            logging.debug("Use XJFrogApiAuth apikey")
+            obj.auth = XJFrogArtApiAuth(apikey=apikey)
+        elif token:
+            logging.debug("Use XJFrogArtBearerAuth token")
+            obj.auth = XJFrogArtBearerAuth(token=token)
+        else:
             auth = kwargs.get("auth")
             obj.auth = auth if auth_type is None else auth_type(*auth)
-        else:
-            logging.debug("Use XJFrogApiAuth")
-            obj.auth = XJFrogArtApiAuth(apikey)
 
         if obj.auth is None and cfg_entry:
             auth = (cfg_entry["username"], cfg_entry["password"])
diff --git a/dohq_artifactory/auth.py b/dohq_artifactory/auth.py
index 214eb2b..52cd628 100644
--- a/dohq_artifactory/auth.py
+++ b/dohq_artifactory/auth.py
@@ -16,3 +16,20 @@ class XJFrogArtApiAuth(AuthBase):
     def __call__(self, r):
         r.headers["X-JFrog-Art-Api"] = self.apikey
         return r
+
+
+class XJFrogArtBearerAuth(AuthBase):
+    """Attaches X-JFrog-Art-Bearer Authentication to the given Request object."""
+
+    def __init__(self, token):
+        self.token = token
+
+    def __eq__(self, other):
+        return all([self.token == getattr(other, "token", None)])
+
+    def __ne__(self, other):
+        return not self == other
+
+    def __call__(self, r):
+        r.headers["Authorization"] = "Bearer " + self.token
+        return r


Test patch:
diff --git a/tests/unit/test_artifactory_path.py b/tests/unit/test_artifactory_path.py
index 5039aa8..db8cb6b 100644
--- a/tests/unit/test_artifactory_path.py
+++ b/tests/unit/test_artifactory_path.py
@@ -66,6 +66,14 @@ class ArtifactoryFlavorTest(unittest.TestCase):
 
     def test_quote_url(self):
         check = self._check_quote_url
+        check(
+            "https://example.com:80/artifactory/foo",
+            "https://example.com:80/artifactory/foo",
+        )
+        check(
+            "https://example.com:80/artifactory/foo/#1",
+            "https://example.com:80/artifactory/foo/%231",
+        )
         check(
             "https://example.com/artifactory/foo", "https://example.com/artifactory/foo"
         )