Skip to content

Commit 4dce62d

Browse files
authored
Merge pull request #279 from Labelbox/ms/iam-integrations
iam integrations
2 parents 3c0abe9 + daf0756 commit 4dce62d

File tree

9 files changed

+193
-6
lines changed

9 files changed

+193
-6
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,12 @@
11
# Changelog
22

3+
# Version 3.4.0 (2021-09-10)
4+
## Added
5+
* New `IAMIntegration` entity
6+
* `Client.create_dataset()` compatibility with delegated access
7+
* `Organization.get_iam_integrations()` to list all integrations available to an org
8+
* `Organization.get_default_iam_integration()` to only get the defaault iam integration
9+
310
# Version 3.3.0 (2021-09-02)
411
## Added
512
* `Dataset.create_data_rows_sync()` for synchronous bulk uploads of data rows

labelbox/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name = "labelbox"
2-
__version__ = "3.3.0"
2+
__version__ = "3.4.0"
33

44
from labelbox.schema.project import Project
55
from labelbox.client import Client

labelbox/client.py

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# type: ignore
22
from datetime import datetime, timezone
33
import json
4+
45
import logging
56
import mimetypes
67
import os
@@ -9,8 +10,9 @@
910
import requests
1011
import requests.exceptions
1112

12-
from labelbox import utils
1313
import labelbox.exceptions
14+
from labelbox import utils
15+
from labelbox import __version__ as SDK_VERSION
1416
from labelbox.orm import query
1517
from labelbox.orm.db_object import DbObject
1618
from labelbox.pagination import PaginatedCollection
@@ -22,8 +24,8 @@
2224
from labelbox.schema.organization import Organization
2325
from labelbox.schema.data_row_metadata import DataRowMetadataOntology
2426
from labelbox.schema.labeling_frontend import LabelingFrontend
27+
from labelbox.schema.iam_integration import IAMIntegration
2528
from labelbox.schema import role
26-
from labelbox import __version__ as SDK_VERSION
2729

2830
logger = logging.getLogger(__name__)
2931

@@ -503,7 +505,7 @@ def _create(self, db_object_type, data):
503505
res = res["create%s" % db_object_type.type_name()]
504506
return db_object_type(self, res)
505507

506-
def create_dataset(self, **kwargs):
508+
def create_dataset(self, iam_integration=IAMIntegration._DEFAULT, **kwargs):
507509
""" Creates a Dataset object on the server.
508510
509511
Attribute values are passed as keyword arguments.
@@ -512,14 +514,52 @@ def create_dataset(self, **kwargs):
512514
>>> dataset = client.create_dataset(name="<dataset_name>", projects=project)
513515
514516
Args:
517+
iam_integration (IAMIntegration) : Uses the default integration.
518+
Optionally specify another integration or set as None to not use delegated access
515519
**kwargs: Keyword arguments with Dataset attribute values.
516520
Returns:
517521
A new Dataset object.
518522
Raises:
519523
InvalidAttributeError: If the Dataset type does not contain
520524
any of the attribute names given in kwargs.
521525
"""
522-
return self._create(Dataset, kwargs)
526+
dataset = self._create(Dataset, kwargs)
527+
528+
if iam_integration == IAMIntegration._DEFAULT:
529+
iam_integration = self.get_organization(
530+
).get_default_iam_integration()
531+
532+
if iam_integration is None:
533+
return dataset
534+
535+
if not isinstance(iam_integration, IAMIntegration):
536+
raise TypeError(
537+
f"iam integration must be a reference an `IAMIntegration` object. Found {type(iam_integration)}"
538+
)
539+
540+
if not iam_integration.valid:
541+
raise ValueError("Integration is not valid. Please select another.")
542+
try:
543+
self.execute(
544+
"""mutation setSignerForDatasetPyApi($signerId: ID!, $datasetId: ID!) {
545+
setSignerForDataset(data: { signerId: $signerId}, where: {id: $datasetId}){id}}
546+
""", {
547+
'signerId': iam_integration.uid,
548+
'datasetId': dataset.uid
549+
})
550+
validation_result = self.execute(
551+
"""mutation validateDatasetPyApi($id: ID!){validateDataset(where: {id : $id}){
552+
valid checks{name, success}}}
553+
""", {'id': dataset.uid})
554+
555+
if not validation_result['validateDataset']['valid']:
556+
raise labelbox.exceptions.LabelboxError(
557+
f"IAMIntegration {validation_result['validateDataset']['checks']['name']} was not successfully added added to the project."
558+
)
559+
except Exception as e:
560+
dataset.delete()
561+
raise e
562+
return dataset
523563

524564
def create_project(self, **kwargs):
525565
""" Creates a Project object on the server.

labelbox/schema/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,4 @@
1818
import labelbox.schema.user
1919
import labelbox.schema.webhook
2020
import labelbox.schema.data_row_metadata
21+
import labelbox.schema.iam_integration

labelbox/schema/dataset.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from labelbox.schema import iam_integration
12
from labelbox import utils
23
import os
34
import json
@@ -43,6 +44,8 @@ class Dataset(DbObject, Updateable, Deletable):
4344
data_rows = Relationship.ToMany("DataRow", False)
4445
created_by = Relationship.ToOne("User", False, "created_by")
4546
organization = Relationship.ToOne("Organization", False)
47+
iam_integration = Relationship.ToOne("IAMIntegration", False,
48+
"iam_integration", "signer")
4649

4750
def create_data_row(self, **kwargs):
4851
""" Creates a single DataRow belonging to this dataset.

labelbox/schema/iam_integration.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
from dataclasses import dataclass
2+
3+
from labelbox.utils import snake_case
4+
from labelbox.orm.db_object import DbObject
5+
from labelbox.orm.model import Field
6+
7+
8+
@dataclass
9+
class AwsIamIntegrationSettings:
10+
role_arn: str
11+
12+
13+
@dataclass
14+
class GcpIamIntegrationSettings:
15+
service_account_email_id: str
16+
read_bucket: str
17+
18+
19+
class IAMIntegration(DbObject):
20+
""" Represents an IAM integration for delegated access
21+
22+
Attributes:
23+
name (str)
24+
updated_at (datetime)
25+
created_at (datetime)
26+
provider (str)
27+
valid (bool)
28+
last_valid_at (datetime)
29+
is_org_default (boolean)
30+
31+
"""
32+
33+
def __init__(self, client, data):
34+
settings = data.pop('settings', None)
35+
if settings is not None:
36+
type_name = settings.pop('__typename')
37+
settings = {snake_case(k): v for k, v in settings.items()}
38+
if type_name == "GcpIamIntegrationSettings":
39+
self.settings = GcpIamIntegrationSettings(**settings)
40+
elif type_name == "AwsIamIntegrationSettings":
41+
self.settings = AwsIamIntegrationSettings(**settings)
42+
else:
43+
self.settings = None
44+
else:
45+
self.settings = None
46+
super().__init__(client, data)
47+
48+
_DEFAULT = "DEFAULT"
49+
50+
name = Field.String("name")
51+
created_at = Field.DateTime("created_at")
52+
updated_at = Field.DateTime("updated_at")
53+
provider = Field.String("provider")
54+
valid = Field.Boolean("valid")
55+
last_valid_at = Field.DateTime("last_valid_at")
56+
is_org_default = Field.Boolean("is_org_default")

labelbox/schema/organization.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from labelbox.exceptions import LabelboxError
44
from labelbox import utils
5-
from labelbox.orm.db_object import DbObject, experimental, query
5+
from labelbox.orm.db_object import DbObject, experimental, query, Entity
66
from labelbox.orm.model import Field, Relationship
77
from labelbox.schema.invite import Invite, InviteLimit, ProjectRole
88
from labelbox.schema.user import User
@@ -129,3 +129,39 @@ def remove_user(self, user: User):
129129
"""mutation DeleteMemberPyApi($%s: ID!) {
130130
updateUser(where: {id: $%s}, data: {deleted: true}) { id deleted }
131131
}""" % (user_id_param, user_id_param), {user_id_param: user.uid})
132+
133+
def get_iam_integrations(self):
134+
"""
135+
Returns all IAM Integrations for an organization
136+
"""
137+
res = self.client.execute(
138+
"""query getAllIntegrationsPyApi { iamIntegrations {
139+
%s
140+
settings {
141+
__typename
142+
... on AwsIamIntegrationSettings {roleArn}
143+
... on GcpIamIntegrationSettings {serviceAccountEmailId readBucket}
144+
}
145+
146+
} } """ % query.results_query_part(Entity.IAMIntegration))
147+
return [
148+
Entity.IAMIntegration(self.client, integration_data)
149+
for integration_data in res['iamIntegrations']
150+
]
151+
152+
def get_default_iam_integration(self):
153+
"""
154+
Returns the default IAM integration for the organization.
155+
Will return None if there are no default integrations for the org.
156+
"""
157+
integrations = self.get_iam_integrations()
158+
default_integration = [
159+
integration for integration in integrations
160+
if integration.is_org_default
161+
]
162+
if len(default_integration) > 1:
163+
raise ValueError(
164+
"Found more than one default signer. Please contact Labelbox to resolve"
165+
)
166+
return None if not len(
167+
default_integration) else default_integration.pop()

tests/integration/test_data_row_metadata.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,7 @@ def test_delete_non_existent_schema_id(datarow, mdo):
209209

210210

211211
@pytest.mark.slow
212+
@pytest.mark.skip("Test is inconsistent.")
212213
def test_large_bulk_delete_non_existent_schema_id(big_dataset, mdo):
213214
deletes = []
214215
n_fields_start = 0
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import requests
2+
import pytest
3+
4+
5+
@pytest.mark.skip("Can only be tested in specific organizations.")
6+
def test_default_integration(client):
7+
# This tests assumes the following:
8+
# 1. gcp delegated access is configured to work with utkarsh-da-test-bucket
9+
# 2. the integration name is gcp test
10+
# 3. This integration is the default
11+
ds = client.create_dataset(name="new_ds")
12+
dr = ds.create_data_row(
13+
row_data=
14+
"gs://utkarsh-da-test-bucket/mathew-schwartz-8rj4sz9YLCI-unsplash.jpg")
15+
assert requests.get(dr.row_data).status_code == 200
16+
assert ds.iam_integration().name == "GCP Test"
17+
ds.delete()
18+
19+
20+
@pytest.mark.skip("Can only be tested in specific organizations.")
21+
def test_non_default_integration(client):
22+
# This tests assumes the following:
23+
# 1. aws delegated access is configured to work with lbox-test-bucket
24+
# 2. an integration called aws is available to the org
25+
integrations = client.get_organization().get_iam_integrations()
26+
integration = [inte for inte in integrations if 'aws' in inte.name][0]
27+
assert integration.valid
28+
ds = client.create_dataset(iam_integration=integration, name="new_ds")
29+
assert ds.iam_integration().name == "aws"
30+
dr = ds.create_data_row(
31+
row_data=
32+
"https://lbox-test-bucket.s3.us-east-1.amazonaws.com/2021_09_08_0hz_Kleki.png"
33+
)
34+
assert requests.get(dr.row_data).status_code == 200
35+
ds.delete()
36+
37+
38+
def test_no_integration(client, image_url):
39+
ds = client.create_dataset(iam_integration=None, name="new_ds")
40+
assert ds.iam_integration() is None
41+
dr = ds.create_data_row(row_data=image_url)
42+
assert requests.get(dr.row_data).status_code == 200
43+
ds.delete()

0 commit comments

Comments
 (0)