Skip to content

Commit d82632f

Browse files
YiyangLisajarinoctavia-squidington-iii
authored
Source Okta: add resource-sets (incremental supported) (#14700)
* Source Okta: add resource-sets (inremental supported) - stop using the deprecated method get_updated_state, use state property and state setter instead - the payload resource-sets is enveloped, _links.next.href contains the cursor * fix: change assert statement with flake formatting * fix unit tests * clean up get_updated_state overriding * rename and correct sample config * correct sample valid and invalid config * fake the token more * fix the invalid_config.json * change the order of stream, hopefully logs is run firstly * fix: remove log stream from configured catalog * fix: bump connector version on okta.md and Dockerfile * auto-bump connector version [ci skip] Co-authored-by: Sajarin <[email protected]> Co-authored-by: Octavia Squidington III <[email protected]>
1 parent 6d9ae93 commit d82632f

File tree

13 files changed

+194
-31
lines changed

13 files changed

+194
-31
lines changed

airbyte-config/init/src/main/resources/seed/source_definitions.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -655,7 +655,7 @@
655655
- name: Okta
656656
sourceDefinitionId: 1d4fdb25-64fc-4569-92da-fcdca79a8372
657657
dockerRepository: airbyte/source-okta
658-
dockerImageTag: 0.1.12
658+
dockerImageTag: 0.1.13
659659
documentationUrl: https://docs.airbyte.io/integrations/sources/okta
660660
icon: okta.svg
661661
sourceType: api

airbyte-config/init/src/main/resources/seed/source_specs.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6197,7 +6197,7 @@
61976197
- - "client_secret"
61986198
oauthFlowOutputParameters:
61996199
- - "access_token"
6200-
- dockerImage: "airbyte/source-okta:0.1.12"
6200+
- dockerImage: "airbyte/source-okta:0.1.13"
62016201
spec:
62026202
documentationUrl: "https://docs.airbyte.io/integrations/sources/okta"
62036203
connectionSpecification:

airbyte-integrations/bases/source-acceptance-test/source_acceptance_test/tests/test_incremental.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,5 +210,7 @@ def test_state_with_abnormally_large_values(self, connector_config, configured_c
210210
records = filter_output(output, type_=Type.RECORD)
211211
states = filter_output(output, type_=Type.STATE)
212212

213-
assert not records, "The sync should produce no records when run with the state with abnormally large values"
213+
assert (
214+
not records
215+
), f"The sync should produce no records when run with the state with abnormally large values {records[0].record.stream}"
214216
assert states, "The sync should produce at least one STATE message"

airbyte-integrations/connectors/source-okta/Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,5 @@ RUN pip install .
1212
ENV AIRBYTE_ENTRYPOINT "python /airbyte/integration_code/main.py"
1313
ENTRYPOINT ["python", "/airbyte/integration_code/main.py"]
1414

15-
LABEL io.airbyte.version=0.1.12
15+
LABEL io.airbyte.version=0.1.13
1616
LABEL io.airbyte.name=airbyte/source-okta

airbyte-integrations/connectors/source-okta/integration_tests/abnormal_state.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
"users": { "lastUpdated": "3021-09-08T07:04:28.000Z" },
33
"groups": { "lastUpdated": "3021-09-08T07:04:28.000Z" },
44
"group_members": { "id": "00uzzzzzzzzzzzzzzzzz" },
5-
"logs": { "published": "3021-09-08T07:04:28.000Z" }
5+
"logs": { "published": "3021-09-08T07:04:28.000Z" },
6+
"resource_sets": { "id": "iamzzzzzzzzzzzzzzzzz" }
67
}

airbyte-integrations/connectors/source-okta/integration_tests/configured_catalog.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -24,18 +24,18 @@
2424
},
2525
{
2626
"stream": {
27-
"name": "logs",
27+
"name": "group_members",
2828
"json_schema": {},
2929
"supported_sync_modes": ["full_refresh", "incremental"]
3030
},
3131
"sync_mode": "incremental",
3232
"destination_sync_mode": "overwrite",
33-
"cursor_field": ["published"],
34-
"primary_key": [["uuid"]]
33+
"cursor_field": ["id"],
34+
"primary_key": [["id"]]
3535
},
3636
{
3737
"stream": {
38-
"name": "group_members",
38+
"name": "resource_sets",
3939
"json_schema": {},
4040
"supported_sync_modes": ["full_refresh", "incremental"]
4141
},
Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
{
2-
"base_url": "invalid url",
3-
"token": "invalid token"
2+
"domain": "myorg",
3+
"start_date": "2022-07-22T00:00:00Z",
4+
"credentials": {
5+
"auth_type": "api_token",
6+
"api_token": "00uItIsFake_DoNotUseTheTokenEoxoRw_2"
7+
}
48
}
Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
{
2-
"base_url": "https://myorg.okta.com",
3-
"token": "xyz123foo325a.fbar"
2+
"domain": "myorg",
3+
"start_date": "2022-07-22T00:00:00Z",
4+
"credentials": {
5+
"auth_type": "api_token",
6+
"api_token": "00uItIsFake_DoNotUseTheTokenEoxoRw_2"
7+
}
48
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
{
2+
"properties": {
3+
"id": {
4+
"type": "string"
5+
},
6+
"label": {
7+
"type": "string"
8+
},
9+
"description": {
10+
"type": "string"
11+
},
12+
"_links": {
13+
"properties": {
14+
"assignee": {
15+
"properties": {
16+
"self": {
17+
"type": ["null", "object"],
18+
"additionalProperties": true,
19+
"properties": {
20+
"href": {
21+
"type": ["null", "string"]
22+
}
23+
},
24+
"description": "gets this Resource Set"
25+
},
26+
"resources": {
27+
"type": ["null", "object"],
28+
"additionalProperties": true,
29+
"properties": {
30+
"href": {
31+
"type": ["null", "string"]
32+
}
33+
},
34+
"description": "gets a paginable list of resources included in this set"
35+
},
36+
"bindings": {
37+
"type": ["null", "object"],
38+
"additionalProperties": true,
39+
"properties": {
40+
"href": {
41+
"type": ["null", "string"]
42+
}
43+
},
44+
"description": "gets a paginable list of admin Role Bindings assigned to this set"
45+
},
46+
"next": {
47+
"type": ["null", "object"],
48+
"additionalProperties": true,
49+
"properties": {
50+
"href": {
51+
"type": ["null", "string"]
52+
}
53+
},
54+
"description": "the link for the next page, 'after' is the query string, the cursor field is id"
55+
}
56+
}
57+
}
58+
},
59+
"type": ["object", "null"]
60+
}
61+
},
62+
"type": "object"
63+
}

airbyte-integrations/connectors/source-okta/source_okta/source.py

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,6 @@ def next_page_token(self, response: requests.Response) -> Optional[Mapping[str,
4545
if "self" in links:
4646
if links["self"]["url"] == next_url:
4747
return None
48-
4948
return query_params
5049

5150
return None
@@ -79,17 +78,19 @@ def backoff_time(self, response: requests.Response) -> Optional[float]:
7978

8079

8180
class IncrementalOktaStream(OktaStream, ABC):
81+
min_id = ""
82+
8283
@property
8384
@abstractmethod
8485
def cursor_field(self) -> str:
8586
pass
8687

8788
def get_updated_state(self, current_stream_state: MutableMapping[str, Any], latest_record: Mapping[str, Any]) -> Mapping[str, Any]:
88-
lowest_date = str(pendulum.datetime.min)
89+
min_cursor_value = self.min_id if self.min_id else str(pendulum.datetime.min)
8990
return {
9091
self.cursor_field: max(
91-
latest_record.get(self.cursor_field, lowest_date),
92-
current_stream_state.get(self.cursor_field, lowest_date),
92+
latest_record.get(self.cursor_field, min_cursor_value),
93+
current_stream_state.get(self.cursor_field, min_cursor_value),
9394
)
9495
}
9596

@@ -117,8 +118,8 @@ def path(self, **kwargs) -> str:
117118
class GroupMembers(IncrementalOktaStream):
118119
cursor_field = "id"
119120
primary_key = "id"
120-
min_user_id = "00u00000000000000000"
121121
use_cache = True
122+
min_id = "00u00000000000000000"
122123

123124
def stream_slices(self, **kwargs):
124125
group_stream = Groups(authenticator=self.authenticator, url_base=self.url_base, start_date=self.start_date)
@@ -135,22 +136,11 @@ def request_params(
135136
stream_slice: Mapping[str, any] = None,
136137
next_page_token: Mapping[str, Any] = None,
137138
) -> MutableMapping[str, Any]:
138-
# Filter param should be ignored SCIM filter expressions can't use the published
139-
# attribute since it may conflict with the logic of the since, after, and until query params.
140-
# Docs: https://developer.okta.com/docs/reference/api/system-log/#expression-filter
141139
params = super(IncrementalOktaStream, self).request_params(stream_state, stream_slice, next_page_token)
142-
latest_entry = stream_state.get(self.cursor_field) if stream_state else self.min_user_id
140+
latest_entry = stream_state.get(self.cursor_field) if stream_state else self.min_id
143141
params["after"] = latest_entry
144142
return params
145143

146-
def get_updated_state(self, current_stream_state: MutableMapping[str, Any], latest_record: Mapping[str, Any]) -> Mapping[str, Any]:
147-
return {
148-
self.cursor_field: max(
149-
latest_record.get(self.cursor_field, self.min_user_id),
150-
current_stream_state.get(self.cursor_field, self.min_user_id),
151-
)
152-
}
153-
154144

155145
class GroupRoleAssignments(OktaStream):
156146
primary_key = "id"
@@ -250,6 +240,45 @@ def request_params(
250240
return params
251241

252242

243+
class ResourceSets(IncrementalOktaStream):
244+
cursor_field = "id"
245+
primary_key = "id"
246+
min_id = "iam00000000000000000"
247+
248+
def path(self, **kwargs) -> str:
249+
return "iam/resource-sets"
250+
251+
def parse_response(
252+
self,
253+
response: requests.Response,
254+
**kwargs,
255+
) -> Iterable[Mapping]:
256+
yield from response.json()["resource-sets"]
257+
258+
def next_page_token(self, response: requests.Response) -> Optional[Mapping[str, Any]]:
259+
# We can't follow the default pagination that takes query from header.links
260+
# Instead, the payload contains _links that offers the next link
261+
body = response.json()
262+
if "_links" in body and "next" in body["_links"] and "href" in body["_links"]["next"]:
263+
next_url = body["_links"]["next"]["href"]
264+
parsed_link = parse.urlparse(next_url)
265+
return dict(parse.parse_qsl(parsed_link.query))
266+
267+
return None
268+
269+
def request_params(
270+
self,
271+
stream_state: Mapping[str, Any],
272+
stream_slice: Mapping[str, any] = None,
273+
next_page_token: Mapping[str, Any] = None,
274+
) -> MutableMapping[str, Any]:
275+
params = super().request_params(stream_state, stream_slice, next_page_token)
276+
latest_entry = stream_state.get(self.cursor_field)
277+
if latest_entry:
278+
params["after"] = latest_entry
279+
return params
280+
281+
253282
class CustomRoles(OktaStream):
254283
# https://developer.okta.com/docs/reference/api/roles/#list-roles
255284
primary_key = "id"
@@ -337,4 +366,5 @@ def streams(self, config: Mapping[str, Any]) -> List[Stream]:
337366
UserRoleAssignments(**initialization_params),
338367
GroupRoleAssignments(**initialization_params),
339368
Permissions(**initialization_params),
369+
ResourceSets(**initialization_params),
340370
]

airbyte-integrations/connectors/source-okta/unit_tests/conftest.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -378,6 +378,26 @@ def logs_instance():
378378
}
379379

380380

381+
@pytest.fixture()
382+
def resource_set_instance(api_url):
383+
"""
384+
Resource set object instance
385+
"""
386+
_id = "iam5xyzmibarA6Afoo7"
387+
return {
388+
"id": _id,
389+
"label": "all users",
390+
"description": "all users",
391+
"created": "2022-07-09T20:58:41.000Z",
392+
"lastUpdated": "2022-07-09T20:58:41.000Z",
393+
"_links": {
394+
"bindings": {"href": f"{url_base}/iam/resource-sets/{_id}/bindings"},
395+
"self": {"href": f"{url_base}/iam/resource-sets/{_id}"},
396+
"resources": {"href": f"{url_base}/iam/resource-sets/{_id}/resources"},
397+
},
398+
}
399+
400+
381401
@pytest.fixture()
382402
def latest_record_instance(url_base, api_url):
383403
"""

airbyte-integrations/connectors/source-okta/unit_tests/test_streams.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
Logs,
2121
OktaStream,
2222
Permissions,
23+
ResourceSets,
2324
UserRoleAssignments,
2425
Users,
2526
)
@@ -126,7 +127,6 @@ def cursor_field(self) -> str:
126127

127128
stream = TestIncrementalOktaStream(url_base=url_base, start_date=start_date)
128129
stream._cursor_field = "lastUpdated"
129-
130130
current_stream_state = {"lastUpdated": "2021-04-21T21:03:55.000Z"}
131131
update_state = stream.get_updated_state(current_stream_state=current_stream_state, latest_record=latest_record_instance)
132132
expected_result = {"lastUpdated": "2022-07-18T07:58:11.000Z"}
@@ -360,3 +360,40 @@ def test_user_role_assignments_slice_stream(
360360
stream = UserRoleAssignments(url_base=url_base, start_date=start_date)
361361
requests_mock.get(f"{api_url}/users?limit=200", json=[users_instance])
362362
assert list(stream.stream_slices()) == [{"user_id": "test_user_id"}]
363+
364+
365+
class TestStreamResourceSets:
366+
def test_resource_sets(self, requests_mock, patch_base_class, resource_set_instance, url_base, api_url, start_date):
367+
stream = ResourceSets(url_base=url_base, start_date=start_date)
368+
record = {"resource-sets": [resource_set_instance]}
369+
requests_mock.get(f"{api_url}/iam/resource-sets", json=record)
370+
inputs = {"sync_mode": SyncMode.incremental}
371+
assert list(stream.read_records(**inputs)) == record["resource-sets"]
372+
373+
def test_resource_sets_parse_response(self, requests_mock, patch_base_class, resource_set_instance, url_base, api_url, start_date):
374+
stream = ResourceSets(url_base=url_base, start_date=start_date)
375+
record = {"resource-sets": [resource_set_instance]}
376+
requests_mock.get(f"{api_url}", json=record)
377+
assert list(stream.parse_response(response=requests.get(f"{api_url}"))) == [resource_set_instance]
378+
379+
def test_resource_sets_next_page_token(self, requests_mock, patch_base_class, resource_set_instance, url_base, api_url, start_date):
380+
stream = ResourceSets(url_base=url_base, start_date=start_date)
381+
cursor = "iam5cursorFybecursor"
382+
response = MagicMock(requests.Response)
383+
next_link = f"{url_base}/iam/resource-sets?after={cursor}"
384+
response.json = MagicMock(return_value={"_links": {"next": {"href": next_link}}, "resource-sets": [resource_set_instance]})
385+
inputs = {"response": response}
386+
result = stream.next_page_token(**inputs)
387+
assert result == {"after": cursor}
388+
389+
response.json = MagicMock(return_value={"resource-sets": [resource_set_instance]})
390+
inputs = {"response": response}
391+
result = stream.next_page_token(**inputs)
392+
assert result is None
393+
394+
def test_resource_sets_request_params(self, requests_mock, patch_base_class, resource_set_instance, url_base, api_url, start_date):
395+
stream = ResourceSets(url_base=url_base, start_date=start_date)
396+
cursor = "iam5cursorFybecursor"
397+
inputs = {"stream_slice": None, "stream_state": {"id": cursor}, "next_page_token": None}
398+
expected_params = {"limit": 200, "after": "iam5cursorFybecursor", "filter": 'id gt "iam5cursorFybecursor"'}
399+
assert stream.request_params(**inputs) == expected_params

docs/integrations/sources/okta.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ The Okta source connector supports the following [sync modes](https://docs.airby
6969
- [System Log](https://developer.okta.com/docs/reference/api/system-log/#get-started)
7070
- [Custom Roles](https://developer.okta.com/docs/reference/api/roles/#list-roles)
7171
- [Permissions](https://developer.okta.com/docs/reference/api/roles/#list-permissions)
72+
- [Resource Sets](https://developer.okta.com/docs/reference/api/roles/#list-resource-sets)
7273

7374
## Performance considerations
7475

@@ -78,6 +79,7 @@ The connector is restricted by normal Okta [requests limitation](https://develop
7879

7980
| Version | Date | Pull Request | Subject |
8081
|:--------|:-----------|:---------------------------------------------------------|:-------------------------------------------------------------------------------|
82+
| 0.1.13 | 2022-08-12 | [14700](https://github.com/airbytehq/airbyte/pull/14700) | Add resource sets |
8183
| 0.1.12 | 2022-08-05 | [15050](https://github.com/airbytehq/airbyte/pull/15050) | Add parameter `start_date` for Logs stream |
8284
| 0.1.11 | 2022-08-03 | [14739](https://github.com/airbytehq/airbyte/pull/14739) | Add permissions for custom roles |
8385
| 0.1.10 | 2022-08-01 | [15179](https://github.com/airbytehq/airbyte/pull/15179) | Fix broken schemas for all streams |

0 commit comments

Comments
 (0)