Skip to content

Commit b28aa50

Browse files
authored
feat: Add an integration subcommand for manipulating integrations (#776)
This commit adds a suite of CLI commands for managing "OAuth" integrations: $ rsconnect integration list $ rsconnect integration show GUID $ rsconnect integration add --template <key> [-N name] [-C key=value ...] $ rsconnect integration edit GUID [-N name] [-C key=value ...] $ rsconnect integration remove GUID $ rsconnect integration templates list $ rsconnect integration templates show --key <key> The `edit` command merges `--config` fields with the existing config (fetching it only when config changes are requested), so users only need to specify fields they want to change. ACLs can be passed via `--allow-user` and `--allow-group`. Unit tests are included, as is autogenerated documentation. Signed-off-by: Aaron Jacobs <aaron.jacobs@posit.co>
1 parent 817b61b commit b28aa50

12 files changed

Lines changed: 867 additions & 0 deletions

File tree

docs/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1616
used automatically. `rsconnect login` sets the server as default unless
1717
`--no-set-default` is passed. `CONNECT_SERVER` still takes precedence.
1818
- New `environment` subcommand for managing execution environments on Connect.
19+
- New `integration` subcommand for managing OAuth integrations on Connect.
1920

2021
### Added
2122

docs/commands/integration.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
::: mkdocs-click
2+
:module: rsconnect.main
3+
:command: integration

mkdocs.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ nav:
4646
- details: commands/details.md
4747
- environment: commands/environment.md
4848
- info: commands/info.md
49+
- integration: commands/integration.md
4950
- list: commands/list.md
5051
- login: commands/login.md
5152
- logout: commands/logout.md

rsconnect/actions_integration.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
"""
2+
Public API for managing OAuth integrations on Posit Connect.
3+
"""
4+
5+
from __future__ import annotations
6+
7+
from typing import Optional, Union
8+
9+
from .api import RSConnectClient, RSConnectServer, SPCSConnectServer
10+
from .models import (
11+
OAuthIntegration,
12+
OAuthIntegrationInput,
13+
OAuthIntegrationPermission,
14+
OAuthIntegrationUpdate,
15+
OAuthTemplate,
16+
)
17+
18+
19+
def list_oauth_integrations(
20+
connect_server: Union[RSConnectServer, SPCSConnectServer],
21+
) -> list[OAuthIntegration]:
22+
with RSConnectClient(connect_server) as client:
23+
return client.oauth_integration_list()
24+
25+
26+
def get_oauth_integration(
27+
connect_server: Union[RSConnectServer, SPCSConnectServer],
28+
guid: str,
29+
) -> OAuthIntegration:
30+
with RSConnectClient(connect_server) as client:
31+
return client.oauth_integration_get(guid)
32+
33+
34+
def create_oauth_integration(
35+
connect_server: Union[RSConnectServer, SPCSConnectServer],
36+
template: str,
37+
config: dict[str, object],
38+
name: Optional[str] = None,
39+
description: Optional[str] = None,
40+
user_guids: Optional[list[str]] = None,
41+
group_guids: Optional[list[str]] = None,
42+
) -> OAuthIntegration:
43+
permissions: list[OAuthIntegrationPermission] = []
44+
for g in user_guids or []:
45+
permissions.append({"user_guid": g, "group_guid": None})
46+
for g in group_guids or []:
47+
permissions.append({"user_guid": None, "group_guid": g})
48+
49+
body: OAuthIntegrationInput = {
50+
"template": template,
51+
"config": config,
52+
}
53+
if name is not None:
54+
body["name"] = name
55+
if description is not None:
56+
body["description"] = description
57+
if permissions:
58+
body["permissions"] = permissions
59+
60+
with RSConnectClient(connect_server) as client:
61+
return client.oauth_integration_create(body)
62+
63+
64+
def update_oauth_integration(
65+
connect_server: Union[RSConnectServer, SPCSConnectServer],
66+
guid: str,
67+
config: Optional[dict[str, object]] = None,
68+
name: Optional[str] = None,
69+
description: Optional[str] = None,
70+
user_guids: Optional[list[str]] = None,
71+
group_guids: Optional[list[str]] = None,
72+
) -> OAuthIntegration:
73+
with RSConnectClient(connect_server) as client:
74+
body: OAuthIntegrationUpdate = {}
75+
if name is not None:
76+
body["name"] = name
77+
if description is not None:
78+
body["description"] = description
79+
if config is not None:
80+
existing = client.oauth_integration_get(guid)
81+
merged_config = dict(existing["config"])
82+
merged_config.update(config)
83+
body["config"] = merged_config
84+
85+
permissions: list[OAuthIntegrationPermission] = []
86+
if user_guids:
87+
for g in user_guids:
88+
permissions.append({"user_guid": g, "group_guid": None})
89+
if group_guids:
90+
for g in group_guids:
91+
permissions.append({"user_guid": None, "group_guid": g})
92+
if permissions:
93+
body["permissions"] = permissions
94+
95+
return client.oauth_integration_update(guid, body)
96+
97+
98+
def delete_oauth_integration(
99+
connect_server: Union[RSConnectServer, SPCSConnectServer],
100+
guid: str,
101+
) -> None:
102+
with RSConnectClient(connect_server) as client:
103+
client.oauth_integration_delete(guid)
104+
105+
106+
def list_oauth_templates(
107+
connect_server: Union[RSConnectServer, SPCSConnectServer],
108+
) -> list[OAuthTemplate]:
109+
with RSConnectClient(connect_server) as client:
110+
return client.oauth_template_list()
111+
112+
113+
def get_oauth_template(
114+
connect_server: Union[RSConnectServer, SPCSConnectServer],
115+
key: str,
116+
) -> OAuthTemplate:
117+
with RSConnectClient(connect_server) as client:
118+
return client.oauth_template_get(key)

rsconnect/api.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,10 @@
8282
EnvironmentUpdateInput,
8383
EnvironmentV1,
8484
ListEntryOutputDTO,
85+
OAuthIntegration,
86+
OAuthIntegrationInput,
87+
OAuthIntegrationUpdate,
88+
OAuthTemplate,
8589
PyInfo,
8690
ServerSettings,
8791
TaskStatusV1,
@@ -782,6 +786,40 @@ def environment_permission_delete(self, env_guid: str, permission_guid: str) ->
782786
)
783787
self._server.handle_bad_response(response, is_httpresponse=True)
784788

789+
def oauth_integration_list(self) -> list[OAuthIntegration]:
790+
response = cast(Union[List[OAuthIntegration], HTTPResponse], self.get("v1/oauth/integrations"))
791+
response = self._server.handle_bad_response(response)
792+
return response
793+
794+
def oauth_integration_get(self, guid: str) -> OAuthIntegration:
795+
response = cast(Union[OAuthIntegration, HTTPResponse], self.get(f"v1/oauth/integrations/{guid}"))
796+
response = self._server.handle_bad_response(response)
797+
return response
798+
799+
def oauth_integration_create(self, body: OAuthIntegrationInput) -> OAuthIntegration:
800+
response = cast(Union[OAuthIntegration, HTTPResponse], self.post("v1/oauth/integrations", body=body))
801+
response = self._server.handle_bad_response(response)
802+
return response
803+
804+
def oauth_integration_update(self, guid: str, body: OAuthIntegrationUpdate) -> OAuthIntegration:
805+
response = cast(Union[OAuthIntegration, HTTPResponse], self.patch(f"v1/oauth/integrations/{guid}", body=body))
806+
response = self._server.handle_bad_response(response)
807+
return response
808+
809+
def oauth_integration_delete(self, guid: str) -> None:
810+
response = cast(HTTPResponse, self.delete(f"v1/oauth/integrations/{guid}", decode_response=False))
811+
self._server.handle_bad_response(response, is_httpresponse=True)
812+
813+
def oauth_template_list(self) -> list[OAuthTemplate]:
814+
response = cast(Union[List[OAuthTemplate], HTTPResponse], self.get("v1/oauth/templates"))
815+
response = self._server.handle_bad_response(response)
816+
return response
817+
818+
def oauth_template_get(self, key: str) -> OAuthTemplate:
819+
response = cast(Union[OAuthTemplate, HTTPResponse], self.get(f"v1/oauth/templates/{key}"))
820+
response = self._server.handle_bad_response(response)
821+
return response
822+
785823
def task_get(
786824
self,
787825
task_id: str,

0 commit comments

Comments
 (0)