Skip to content

Commit 2007790

Browse files
authored
Merge branch 'master' into bcwu-fix-htmlextrafiles
2 parents 6dbeab9 + 4838b11 commit 2007790

14 files changed

+273
-46
lines changed

.github/pull_request_template.md

+31-17
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,35 @@
1-
### Description
1+
<!--- Provide a general summary of your changes in the title. -->
22

3-
> Include a brief description of the problem being solved and why this
4-
> approach was chosen. Mention risks or shortcomings with this solution.
5-
> Provide relevant background information such as the associated issue, links
6-
> to design documents, screenshots, and performance measurements.
7-
>
8-
> Even small changes deserve a little attention to detail. Put your change in
9-
> context.
3+
## Description
4+
<!--- Please describe the problem you are addressing and your proposed changes. -->
105

11-
Connected to #
6+
## Motivation and Context
7+
<!--- Even small changes deserve a little attention to detail. -->
8+
<!--- Why is this change required? What problem does it solve? -->
9+
<!--- If it fixes an open issue, please link to the issue here. -->
10+
<!--- Example: "Addresses Issue # 1" -->
1211

13-
### Testing Notes / Validation Steps
12+
## How Has This Been Tested?
13+
<!--- Please describe in detail how you tested your changes. -->
14+
<!--- Automation should be used to verify that a change remains in place. -->
15+
<!--- Automated tests should be included in this pull request. -->
1416

15-
> Explain how this change has been tested and what cases/conditions are
16-
> covered. Enumerate the steps someone might take to manually exercise this
17-
> change. Detail is important!
18-
>
19-
> You can recommend one-time manual testing when someone validates your
20-
> changes. Rely on automation when trying to verify that a change remains in
21-
> place. Automated tests should be included in this PR.
17+
<!--- If applicable, enumerate the steps that someone can take to exercise this change manually. -->
18+
<!--- Please include details of your testing environment. -->
19+
<!--- Detail is important! -->
20+
21+
### Screenshots (if appropriate):
22+
23+
## Types of Changes
24+
<!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply. -->
25+
- [ ] Bug fix (non-breaking change which fixes an issue)
26+
- [ ] New feature (non-breaking change which adds functionality)
27+
- [ ] Breaking change (fix or feature that would cause existing functionality to change)
28+
29+
## Checklist:
30+
<!--- Go over all the following points, and put an `x` in all the boxes that apply: -->
31+
<!--- If you need clarification on any of these, feel free to ask. We're here to help! -->
32+
- [ ] I have read the **CONTRIBUTING** document.
33+
- [ ] My code follows the code style of this project.
34+
- [ ] I have updated the documentation as needed.
35+
- [ ] I have added tests to cover my changes.

.gitignore

+8-7
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,26 @@
22
.DS_Store
33
.coverage
44
.vagrant
5-
/.vscode/
65
/*.egg
76
/*.egg-info
87
/*.eggs
98
/.conda/
109
/.idea/
1110
/.jupyter/
1211
/.local/
13-
/.venv/
1412
/.pipenv-requires
13+
/.venv/
14+
/.vscode/
1515
/build/
1616
/dist/
17+
/docs/docs/changelog.md
18+
/docs/docs/index.md
1719
/node_modules/
1820
/notebooks*/
21+
/rsconnect-build
22+
/rsconnect-build-test
1923
/rsconnect/version.py
20-
htmlcov
2124
/tests/testdata/**/rsconnect-python/
25+
htmlcov
2226
test-home/
23-
/docs/docs/index.md
24-
/docs/docs/changelog.md
25-
/rsconnect-build
26-
/rsconnect-build-test
27+
venv

CHANGELOG.md

+10
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file.
44
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
55
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
66

7+
## [Unreleased]
8+
9+
### Added
10+
11+
- The `--cacert` option now supports certificate files encoded in the Distinguished Encoding Rules (DER) binary format. Certificate files with DER encoding must end in a `.cer` or `.der` suffix.
12+
13+
### Changed
14+
15+
- The `--cacert` option now requires that Privacy Enhanced Mail (PEM) formatted certificate files end in a `.ca-bundle`, `.crt`, `.key`, or `.pem` suffix.
16+
717
## [1.14.0] - 2023-01-19
818

919
### Changed

rsconnect/api.py

+7-7
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@
1818

1919
import re
2020
from warnings import warn
21-
from six import text_type
2221
import gc
2322

2423
from . import validation
24+
from .certificates import read_certificate_file
2525
from .http_support import HTTPResponse, HTTPServer, append_to_path, CookieJar
2626
from .log import logger, connect_logger, cls_logged, console_logger
2727
from .models import AppModes
@@ -360,7 +360,7 @@ def __init__(
360360
url: str = None,
361361
api_key: str = None,
362362
insecure: bool = False,
363-
cacert: IO = None,
363+
cacert: str = None,
364364
ca_data: str = None,
365365
cookies=None,
366366
account=None,
@@ -415,7 +415,7 @@ def setup_remote_server(
415415
url: str = None,
416416
api_key: str = None,
417417
insecure: bool = False,
418-
cacert: IO = None,
418+
cacert: str = None,
419419
ca_data: str = None,
420420
account_name: str = None,
421421
token: str = None,
@@ -433,7 +433,7 @@ def setup_remote_server(
433433
)
434434

435435
if cacert and not ca_data:
436-
ca_data = text_type(cacert.read())
436+
ca_data = read_certificate_file(cacert)
437437

438438
server_data = ServerStore().resolve(name, url)
439439
if server_data.from_store:
@@ -507,7 +507,7 @@ def validate_server(
507507
url: str = None,
508508
api_key: str = None,
509509
insecure: bool = False,
510-
cacert: IO = None,
510+
cacert: str = None,
511511
api_key_is_required: bool = False,
512512
account_name: str = None,
513513
token: str = None,
@@ -528,7 +528,7 @@ def validate_connect_server(
528528
url: str = None,
529529
api_key: str = None,
530530
insecure: bool = False,
531-
cacert: IO = None,
531+
cacert: str = None,
532532
api_key_is_required: bool = False,
533533
**kwargs
534534
):
@@ -551,7 +551,7 @@ def validate_connect_server(
551551

552552
ca_data = None
553553
if cacert:
554-
ca_data = text_type(cacert.read())
554+
ca_data = read_certificate_file(cacert)
555555
api_key = api_key or self.remote_server.api_key
556556
insecure = insecure or self.remote_server.insecure
557557
if not ca_data:

rsconnect/certificates.py

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
from pathlib import Path
2+
3+
BINARY_ENCODED_FILETYPES = [".cer", ".der"]
4+
TEXT_ENCODED_FILETYPES = [".ca-bundle", ".crt", ".key", ".pem"]
5+
6+
7+
def read_certificate_file(location: str):
8+
"""Reads a certificate file from disk.
9+
10+
The file type (suffix) is used to determine the file encoding.
11+
Assumption are made based on standard SSL practices.
12+
13+
Files ending in '.cer' and '.der' are assumed DER (Distinguished
14+
Encoding Rules) files encoded in binary format.
15+
16+
Files ending in '.ca-bundle', '.crt', '.key', and '.pem' are PEM
17+
(Privacy Enhanced Mail) files encoded in plain-text format.
18+
"""
19+
20+
path = Path(location)
21+
suffix = path.suffix
22+
23+
if suffix in BINARY_ENCODED_FILETYPES:
24+
with open(path, "rb") as file:
25+
return file.read()
26+
27+
if suffix in TEXT_ENCODED_FILETYPES:
28+
with open(path, "r") as file:
29+
return file.read()
30+
31+
types = BINARY_ENCODED_FILETYPES + TEXT_ENCODED_FILETYPES
32+
types = sorted(types)
33+
types = [f"'{_}'" for _ in types]
34+
human_readable_string = ", ".join(types[:-1]) + ", or " + types[-1]
35+
raise RuntimeError(
36+
f"The certificate file type is not recognized. Expected {human_readable_string}. Found '{suffix}'."
37+
)

rsconnect/main.py

+20-15
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@
66
import typing
77
import textwrap
88
import click
9-
from six import text_type
109
from os.path import abspath, dirname, exists, isdir, join
1110
from functools import wraps
11+
12+
from rsconnect.certificates import read_certificate_file
13+
1214
from .environment import EnvironmentException
1315
from .exception import RSConnectException
1416
from .actions import (
@@ -129,7 +131,7 @@ def server_args(func):
129131
"--cacert",
130132
"-c",
131133
envvar="CONNECT_CA_CERTIFICATE",
132-
type=click.File(),
134+
type=click.Path(exists=True, file_okay=True, dir_okay=False),
133135
help="The path to trusted TLS CA certificates.",
134136
)
135137
@click.option("--verbose", "-v", is_flag=True, help="Print detailed messages.")
@@ -269,7 +271,7 @@ def _test_server_and_api(server, api_key, insecure, ca_cert):
269271
:return: a tuple containing an appropriate ConnectServer object and the username
270272
of the user the API key represents (or None, if no key was provided).
271273
"""
272-
ca_data = ca_cert and text_type(ca_cert.read())
274+
ca_data = ca_cert and ca_cert.read()
273275
me = None
274276

275277
with cli_feedback("Checking %s" % server):
@@ -312,7 +314,7 @@ def _test_rstudio_creds(server: api.PositServer):
312314
"--cacert",
313315
"-c",
314316
envvar="CONNECT_CA_CERTIFICATE",
315-
type=click.File(),
317+
type=click.Path(exists=True, file_okay=True, dir_okay=False),
316318
help="The path to trusted TLS CA certificates.",
317319
)
318320
@click.option(
@@ -344,7 +346,10 @@ def bootstrap(
344346
logger.debug("Generated JWT:\n" + bootstrap_token)
345347

346348
logger.debug("Insecure: " + str(insecure))
347-
ca_data = cacert and text_type(cacert.read())
349+
350+
ca_data = None
351+
if cacert:
352+
ca_data = read_certificate_file(cacert)
348353

349354
with cli_feedback("", stderr=True):
350355
connect_server = RSConnectServer(
@@ -398,7 +403,7 @@ def bootstrap(
398403
"--cacert",
399404
"-c",
400405
envvar="CONNECT_CA_CERTIFICATE",
401-
type=click.File(),
406+
type=click.Path(exists=True, file_okay=True, dir_okay=False),
402407
help="The path to trusted TLS CA certificates.",
403408
)
404409
@click.option("--verbose", "-v", is_flag=True, help="Print detailed messages.")
@@ -1674,7 +1679,7 @@ def content():
16741679
"--cacert",
16751680
"-c",
16761681
envvar="CONNECT_CA_CERTIFICATE",
1677-
type=click.File(),
1682+
type=click.Path(exists=True, file_okay=True, dir_okay=False),
16781683
help="The path to trusted TLS CA certificates.",
16791684
)
16801685
@click.option(
@@ -1768,7 +1773,7 @@ def content_search(
17681773
"--cacert",
17691774
"-c",
17701775
envvar="CONNECT_CA_CERTIFICATE",
1771-
type=click.File(),
1776+
type=click.Path(exists=True, file_okay=True, dir_okay=False),
17721777
help="The path to trusted TLS CA certificates.",
17731778
)
17741779
@click.option(
@@ -1819,7 +1824,7 @@ def content_describe(name, server, api_key, insecure, cacert, guid, verbose):
18191824
"--cacert",
18201825
"-c",
18211826
envvar="CONNECT_CA_CERTIFICATE",
1822-
type=click.File(),
1827+
type=click.Path(exists=True, file_okay=True, dir_okay=False),
18231828
help="The path to trusted TLS CA certificates.",
18241829
)
18251830
@click.option(
@@ -1888,7 +1893,7 @@ def build():
18881893
"--cacert",
18891894
"-c",
18901895
envvar="CONNECT_CA_CERTIFICATE",
1891-
type=click.File(),
1896+
type=click.Path(exists=True, file_okay=True, dir_okay=False),
18921897
help="The path to trusted TLS CA certificates.",
18931898
)
18941899
@click.option(
@@ -1942,7 +1947,7 @@ def add_content_build(name, server, api_key, insecure, cacert, guid, verbose):
19421947
"--cacert",
19431948
"-c",
19441949
envvar="CONNECT_CA_CERTIFICATE",
1945-
type=click.File(),
1950+
type=click.Path(exists=True, file_okay=True, dir_okay=False),
19461951
help="The path to trusted TLS CA certificates.",
19471952
)
19481953
@click.option(
@@ -2005,7 +2010,7 @@ def remove_content_build(name, server, api_key, insecure, cacert, guid, all, pur
20052010
"--cacert",
20062011
"-c",
20072012
envvar="CONNECT_CA_CERTIFICATE",
2008-
type=click.File(),
2013+
type=click.Path(exists=True, file_okay=True, dir_okay=False),
20092014
help="The path to trusted TLS CA certificates.",
20102015
)
20112016
@click.option("--status", type=click.Choice(BuildStatus._all), help="Filter results by status of the build operation.")
@@ -2053,7 +2058,7 @@ def list_content_build(name, server, api_key, insecure, cacert, status, guid, ve
20532058
"--cacert",
20542059
"-c",
20552060
envvar="CONNECT_CA_CERTIFICATE",
2056-
type=click.File(),
2061+
type=click.Path(exists=True, file_okay=True, dir_okay=False),
20572062
help="The path to trusted TLS CA certificates.",
20582063
)
20592064
@click.option(
@@ -2104,7 +2109,7 @@ def get_build_history(name, server, api_key, insecure, cacert, guid, verbose):
21042109
"--cacert",
21052110
"-c",
21062111
envvar="CONNECT_CA_CERTIFICATE",
2107-
type=click.File(),
2112+
type=click.Path(exists=True, file_okay=True, dir_okay=False),
21082113
help="The path to trusted TLS CA certificates.",
21092114
)
21102115
@click.option(
@@ -2167,7 +2172,7 @@ def get_build_logs(name, server, api_key, insecure, cacert, guid, task_id, forma
21672172
"--cacert",
21682173
"-c",
21692174
envvar="CONNECT_CA_CERTIFICATE",
2170-
type=click.File(),
2175+
type=click.Path(exists=True, file_okay=True, dir_okay=False),
21712176
help="The path to trusted TLS CA certificates.",
21722177
)
21732178
@click.option(

tests/test_certificates.py

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
from tempfile import NamedTemporaryFile
2+
from unittest import TestCase
3+
4+
from rsconnect.certificates import read_certificate_file
5+
6+
7+
class ParseCertificateFileTestCase(TestCase):
8+
9+
def test_parse_certificate_file_ca_bundle(self):
10+
res = read_certificate_file("tests/testdata/certificates/localhost.ca-bundle")
11+
self.assertTrue(res)
12+
13+
def test_parse_certificate_file_cer(self):
14+
res = read_certificate_file("tests/testdata/certificates/localhost.cer")
15+
self.assertTrue(res)
16+
17+
def test_parse_certificate_file_crt(self):
18+
res = read_certificate_file("tests/testdata/certificates/localhost.crt")
19+
self.assertTrue(res)
20+
21+
def test_parse_certificate_file_der(self):
22+
res = read_certificate_file("tests/testdata/certificates/localhost.der")
23+
self.assertTrue(res)
24+
25+
def test_parse_certificate_file_key(self):
26+
res = read_certificate_file("tests/testdata/certificates/localhost.key")
27+
self.assertTrue(res)
28+
29+
def test_parse_certificate_file_pem(self):
30+
res = read_certificate_file("tests/testdata/certificates/localhost.pem")
31+
self.assertTrue(res)
32+
33+
def test_parse_certificate_file_csr(self):
34+
with self.assertRaises(RuntimeError):
35+
read_certificate_file("tests/testdata/certificates/localhost.csr")
36+
37+
def test_parse_certificate_file_invalid(self):
38+
with NamedTemporaryFile() as tmpfile:
39+
with self.assertRaises(RuntimeError):
40+
read_certificate_file(tmpfile.name)

0 commit comments

Comments
 (0)