zou 0.19.14__py3-none-any.whl → 0.20.11__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- zou/__init__.py +1 -1
- zou/app/__init__.py +10 -2
- zou/app/api.py +2 -0
- zou/app/blueprints/assets/__init__.py +22 -0
- zou/app/blueprints/assets/resources.py +241 -4
- zou/app/blueprints/auth/__init__.py +4 -0
- zou/app/blueprints/auth/resources.py +154 -22
- zou/app/blueprints/breakdown/resources.py +4 -4
- zou/app/blueprints/chats/__init__.py +22 -0
- zou/app/blueprints/chats/resources.py +199 -0
- zou/app/blueprints/comments/resources.py +36 -19
- zou/app/blueprints/crud/__init__.py +12 -0
- zou/app/blueprints/crud/attachment_file.py +14 -5
- zou/app/blueprints/crud/base.py +29 -28
- zou/app/blueprints/crud/chat.py +13 -0
- zou/app/blueprints/crud/chat_message.py +13 -0
- zou/app/blueprints/crud/comments.py +85 -29
- zou/app/blueprints/crud/custom_action.py +1 -1
- zou/app/blueprints/crud/day_off.py +47 -9
- zou/app/blueprints/crud/department.py +1 -25
- zou/app/blueprints/crud/entity.py +46 -5
- zou/app/blueprints/crud/entity_type.py +13 -1
- zou/app/blueprints/crud/event.py +1 -1
- zou/app/blueprints/crud/file_status.py +1 -1
- zou/app/blueprints/crud/metadata_descriptor.py +24 -10
- zou/app/blueprints/crud/organisation.py +22 -5
- zou/app/blueprints/crud/output_file.py +1 -1
- zou/app/blueprints/crud/output_type.py +1 -1
- zou/app/blueprints/crud/person.py +32 -24
- zou/app/blueprints/crud/playlist.py +1 -1
- zou/app/blueprints/crud/preview_background_file.py +6 -7
- zou/app/blueprints/crud/preview_file.py +1 -1
- zou/app/blueprints/crud/project.py +14 -6
- zou/app/blueprints/crud/project_status.py +1 -1
- zou/app/blueprints/crud/schedule_item.py +4 -2
- zou/app/blueprints/crud/software.py +1 -1
- zou/app/blueprints/crud/status_automation.py +1 -1
- zou/app/blueprints/crud/studio.py +33 -0
- zou/app/blueprints/crud/task.py +47 -3
- zou/app/blueprints/crud/task_status.py +1 -1
- zou/app/blueprints/crud/task_type.py +4 -4
- zou/app/blueprints/crud/working_file.py +4 -8
- zou/app/blueprints/events/resources.py +13 -12
- zou/app/blueprints/export/csv/assets.py +15 -6
- zou/app/blueprints/export/csv/edits.py +15 -5
- zou/app/blueprints/export/csv/playlists.py +1 -1
- zou/app/blueprints/export/csv/shots.py +15 -5
- zou/app/blueprints/export/csv/time_spents.py +1 -1
- zou/app/blueprints/files/resources.py +22 -23
- zou/app/blueprints/index/resources.py +38 -29
- zou/app/blueprints/news/resources.py +25 -11
- zou/app/blueprints/persons/__init__.py +5 -2
- zou/app/blueprints/persons/resources.py +126 -120
- zou/app/blueprints/previews/__init__.py +18 -8
- zou/app/blueprints/previews/resources.py +569 -328
- zou/app/blueprints/projects/resources.py +1 -1
- zou/app/blueprints/search/resources.py +18 -6
- zou/app/blueprints/shots/__init__.py +5 -0
- zou/app/blueprints/shots/resources.py +134 -4
- zou/app/blueprints/source/__init__.py +6 -6
- zou/app/blueprints/source/csv/assets.py +10 -3
- zou/app/blueprints/source/csv/base.py +1 -1
- zou/app/blueprints/source/csv/edits.py +10 -3
- zou/app/blueprints/source/csv/shots.py +10 -3
- zou/app/blueprints/source/{edl.py → otio.py} +84 -41
- zou/app/blueprints/tasks/__init__.py +3 -2
- zou/app/blueprints/tasks/resources.py +83 -52
- zou/app/blueprints/user/__init__.py +9 -0
- zou/app/blueprints/user/resources.py +170 -12
- zou/app/config.py +10 -0
- zou/app/mixin.py +6 -5
- zou/app/models/attachment_file.py +10 -4
- zou/app/models/base.py +18 -13
- zou/app/models/build_job.py +7 -4
- zou/app/models/chat.py +44 -0
- zou/app/models/chat_message.py +37 -0
- zou/app/models/comment.py +1 -0
- zou/app/models/day_off.py +3 -0
- zou/app/models/entity.py +4 -6
- zou/app/models/entity_type.py +2 -0
- zou/app/models/organisation.py +14 -15
- zou/app/models/person.py +6 -1
- zou/app/models/project.py +3 -0
- zou/app/models/search_filter.py +11 -0
- zou/app/models/search_filter_group.py +10 -0
- zou/app/models/serializer.py +17 -17
- zou/app/models/status_automation.py +2 -0
- zou/app/models/studio.py +13 -0
- zou/app/models/subscription.py +2 -2
- zou/app/models/task.py +6 -1
- zou/app/models/task_status.py +1 -0
- zou/app/models/task_type.py +1 -0
- zou/app/models/working_file.py +1 -1
- zou/app/services/assets_service.py +101 -14
- zou/app/services/auth_service.py +17 -44
- zou/app/services/breakdown_service.py +37 -5
- zou/app/services/chats_service.py +279 -0
- zou/app/services/comments_service.py +110 -65
- zou/app/services/concepts_service.py +4 -12
- zou/app/services/deletion_service.py +43 -30
- zou/app/services/edits_service.py +5 -11
- zou/app/services/emails_service.py +4 -4
- zou/app/services/entities_service.py +17 -2
- zou/app/services/events_service.py +12 -4
- zou/app/services/exception.py +5 -5
- zou/app/services/names_service.py +7 -2
- zou/app/services/news_service.py +17 -9
- zou/app/services/persons_service.py +38 -21
- zou/app/services/playlists_service.py +8 -7
- zou/app/services/preview_files_service.py +137 -10
- zou/app/services/projects_service.py +5 -14
- zou/app/services/shots_service.py +221 -49
- zou/app/services/sync_service.py +46 -42
- zou/app/services/tasks_service.py +185 -46
- zou/app/services/time_spents_service.py +67 -20
- zou/app/services/user_service.py +350 -107
- zou/app/stores/auth_tokens_store.py +2 -1
- zou/app/stores/file_store.py +18 -0
- zou/app/stores/publisher_store.py +7 -7
- zou/app/stores/queue_store.py +1 -0
- zou/app/swagger.py +36 -20
- zou/app/utils/cache.py +2 -0
- zou/app/utils/commands.py +104 -7
- zou/app/utils/csv_utils.py +1 -4
- zou/app/utils/date_helpers.py +33 -17
- zou/app/utils/dbhelpers.py +14 -1
- zou/app/utils/emails.py +2 -2
- zou/app/utils/fido.py +22 -0
- zou/app/utils/flask.py +1 -0
- zou/app/utils/query.py +54 -6
- zou/app/utils/redis.py +11 -0
- zou/app/utils/saml.py +51 -0
- zou/app/utils/string.py +2 -0
- zou/app/utils/thumbnail.py +4 -2
- zou/cli.py +76 -18
- zou/debug.py +4 -2
- zou/event_stream.py +122 -165
- zou/job_settings.py +1 -0
- zou/migrations/env.py +0 -0
- zou/migrations/utils/base.py +6 -6
- zou/migrations/versions/1bb55759146f_add_table_studio.py +67 -0
- zou/migrations/versions/1fab8c420678_add_attachments_to_message_chats.py +56 -0
- zou/migrations/versions/23122f290ca2_add_entity_chat_models.py +149 -0
- zou/migrations/versions/32f134ff1201_add_is_shared_flag_to_filters.py +33 -0
- zou/migrations/versions/57222395f2be_add_statusautomation_import_last_revision.py +41 -0
- zou/migrations/versions/59a7445a966c_add_entity_is_shared.py +41 -0
- zou/migrations/versions/5b980f0dc365_add_comment_links.py +35 -0
- zou/migrations/versions/680c64565f9d_for_searchfiltergroup_is_shared.py +35 -0
- zou/migrations/versions/8e67c183bed7_add_preference_fields.py +71 -0
- zou/migrations/versions/92b40d79ad3f_allow_message_attachments.py +38 -0
- zou/migrations/versions/971dbf5a0faf_add_short_name_for_asset_type_entity_.py +33 -0
- zou/migrations/versions/9b85c14fa8a7_add_day_off_new_columns.py +68 -0
- zou/migrations/versions/9d3bb33c6fc6_add_department_keys_to_filter_models.py +73 -0
- zou/migrations/versions/a252a094e977_add_descriptions_for_entities_tasks_and_.py +40 -0
- zou/migrations/versions/be56dc0fb760_for_is_shared_disallow_nullable.py +102 -0
- zou/migrations/versions/ca28796a2a62_add_is_done_field_to_the_task_model.py +108 -0
- zou/migrations/versions/f344b867a911_for_description_of_entity_task_working_.py +75 -0
- zou/remote/config_payload.py +2 -1
- zou/utils/movie.py +14 -4
- {zou-0.19.14.dist-info → zou-0.20.11.dist-info}/METADATA +75 -69
- {zou-0.19.14.dist-info → zou-0.20.11.dist-info}/RECORD +164 -135
- {zou-0.19.14.dist-info → zou-0.20.11.dist-info}/WHEEL +1 -1
- {zou-0.19.14.dist-info → zou-0.20.11.dist-info}/LICENSE +0 -0
- {zou-0.19.14.dist-info → zou-0.20.11.dist-info}/entry_points.txt +0 -0
- {zou-0.19.14.dist-info → zou-0.20.11.dist-info}/top_level.txt +0 -0
zou/__init__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.
|
|
1
|
+
__version__ = "0.20.11"
|
zou/app/__init__.py
CHANGED
|
@@ -41,6 +41,9 @@ from zou.app.utils.flask import (
|
|
|
41
41
|
wrong_auth_handler,
|
|
42
42
|
)
|
|
43
43
|
|
|
44
|
+
from zou.app.utils.saml import saml_client_for
|
|
45
|
+
from zou.app.utils.fido import get_fido_server
|
|
46
|
+
|
|
44
47
|
app = Flask(__name__)
|
|
45
48
|
app.json = ORJSONProvider(app)
|
|
46
49
|
app.request_class.user_agent_class = ParsedUserAgent
|
|
@@ -64,6 +67,11 @@ swagger = Swagger(
|
|
|
64
67
|
app, template=swagger.swagger_template, config=swagger.swagger_config
|
|
65
68
|
)
|
|
66
69
|
|
|
70
|
+
if config.SAML_ENABLED:
|
|
71
|
+
app.extensions["saml_client"] = saml_client_for(config.SAML_METADATA_URL)
|
|
72
|
+
|
|
73
|
+
app.extensions["fido_server"] = get_fido_server()
|
|
74
|
+
|
|
67
75
|
|
|
68
76
|
@app.teardown_appcontext
|
|
69
77
|
def shutdown_session(exception=None):
|
|
@@ -88,7 +96,7 @@ def id_parameter_format_error(error):
|
|
|
88
96
|
|
|
89
97
|
@app.errorhandler(WrongParameterException)
|
|
90
98
|
def wrong_parameter(error):
|
|
91
|
-
return jsonify(error=True, message=str(error)), 400
|
|
99
|
+
return jsonify(error=True, message=str(error), data=error.dict), 400
|
|
92
100
|
|
|
93
101
|
|
|
94
102
|
@app.errorhandler(ExpiredSignatureError)
|
|
@@ -126,7 +134,7 @@ def indexer_key_error(error):
|
|
|
126
134
|
raise error
|
|
127
135
|
|
|
128
136
|
|
|
129
|
-
if
|
|
137
|
+
if config.DEBUG:
|
|
130
138
|
|
|
131
139
|
@app.errorhandler(Exception)
|
|
132
140
|
def server_error(error):
|
zou/app/api.py
CHANGED
|
@@ -8,6 +8,7 @@ from flask import Blueprint
|
|
|
8
8
|
from zou.app.blueprints.assets import blueprint as assets_blueprint
|
|
9
9
|
from zou.app.blueprints.auth import blueprint as auth_blueprint
|
|
10
10
|
from zou.app.blueprints.breakdown import blueprint as breakdown_blueprint
|
|
11
|
+
from zou.app.blueprints.chats import blueprint as chats_blueprint
|
|
11
12
|
from zou.app.blueprints.comments import blueprint as comments_blueprint
|
|
12
13
|
from zou.app.blueprints.crud import blueprint as crud_blueprint
|
|
13
14
|
from zou.app.blueprints.entities import blueprint as entities_blueprint
|
|
@@ -49,6 +50,7 @@ def configure_api_routes(app):
|
|
|
49
50
|
app.register_blueprint(auth_blueprint)
|
|
50
51
|
app.register_blueprint(assets_blueprint)
|
|
51
52
|
app.register_blueprint(breakdown_blueprint)
|
|
53
|
+
app.register_blueprint(chats_blueprint)
|
|
52
54
|
app.register_blueprint(comments_blueprint)
|
|
53
55
|
app.register_blueprint(crud_blueprint)
|
|
54
56
|
app.register_blueprint(entities_blueprint)
|
|
@@ -21,6 +21,11 @@ from zou.app.blueprints.assets.resources import (
|
|
|
21
21
|
ProjectAssetTypeAssetsResource,
|
|
22
22
|
ProjectAssetTypesResource,
|
|
23
23
|
ShotAssetTypesResource,
|
|
24
|
+
SetSharedProjectAssetsResource,
|
|
25
|
+
SetSharedProjectAssetTypeAssetsResource,
|
|
26
|
+
SetSharedAssetsResource,
|
|
27
|
+
ProjectAssetsSharedUsedResource,
|
|
28
|
+
ProjectEpisodeAssetsSharedUsedResource,
|
|
24
29
|
)
|
|
25
30
|
|
|
26
31
|
|
|
@@ -59,6 +64,23 @@ routes = [
|
|
|
59
64
|
("/data/projects/<project_id>/asset-types", ProjectAssetTypesResource),
|
|
60
65
|
("/data/shots/<shot_id>/asset-types", ShotAssetTypesResource),
|
|
61
66
|
("/data/projects/<project_id>/assets", ProjectAssetsResource),
|
|
67
|
+
("/actions/assets/share", SetSharedAssetsResource),
|
|
68
|
+
(
|
|
69
|
+
"/actions/projects/<project_id>/assets/share",
|
|
70
|
+
SetSharedProjectAssetsResource,
|
|
71
|
+
),
|
|
72
|
+
(
|
|
73
|
+
"/actions/projects/<project_id>/asset-types/<asset_type_id>/assets/share",
|
|
74
|
+
SetSharedProjectAssetTypeAssetsResource,
|
|
75
|
+
),
|
|
76
|
+
(
|
|
77
|
+
"/data/projects/<project_id>/assets/shared-used",
|
|
78
|
+
ProjectAssetsSharedUsedResource,
|
|
79
|
+
),
|
|
80
|
+
(
|
|
81
|
+
"/data/projects/<project_id>/episodes/<episode_id>/assets/shared-used",
|
|
82
|
+
ProjectEpisodeAssetsSharedUsedResource,
|
|
83
|
+
),
|
|
62
84
|
]
|
|
63
85
|
|
|
64
86
|
blueprint = Blueprint("assets", "assets")
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from flask import request
|
|
2
|
-
from flask_restful import Resource
|
|
2
|
+
from flask_restful import Resource, inputs
|
|
3
3
|
from flask_jwt_extended import jwt_required
|
|
4
4
|
|
|
5
5
|
from zou.app.utils import permissions, query
|
|
@@ -21,7 +21,11 @@ def check_criterion_access(criterions):
|
|
|
21
21
|
elif "episode_id" in criterions:
|
|
22
22
|
episode_id = criterions.get("episode_id", None)
|
|
23
23
|
project_id = shots_service.get_episode(episode_id)["project_id"]
|
|
24
|
-
|
|
24
|
+
|
|
25
|
+
if "project_id" in criterions:
|
|
26
|
+
user_service.check_project_access(project_id)
|
|
27
|
+
|
|
28
|
+
return True
|
|
25
29
|
|
|
26
30
|
|
|
27
31
|
class AssetResource(Resource, ArgsMixin):
|
|
@@ -106,7 +110,10 @@ class AllAssetsResource(Resource):
|
|
|
106
110
|
criterions["assigned_to"] = persons_service.get_current_user()[
|
|
107
111
|
"id"
|
|
108
112
|
]
|
|
109
|
-
return assets_service.get_assets(
|
|
113
|
+
return assets_service.get_assets(
|
|
114
|
+
criterions,
|
|
115
|
+
is_admin=permissions.has_admin_permissions(),
|
|
116
|
+
)
|
|
110
117
|
|
|
111
118
|
|
|
112
119
|
class AllAssetsAliasResource(AllAssetsResource):
|
|
@@ -431,6 +438,7 @@ class NewAssetResource(Resource, ArgsMixin):
|
|
|
431
438
|
- name
|
|
432
439
|
- description
|
|
433
440
|
- data
|
|
441
|
+
- is_shared
|
|
434
442
|
- source_id
|
|
435
443
|
properties:
|
|
436
444
|
name:
|
|
@@ -439,6 +447,8 @@ class NewAssetResource(Resource, ArgsMixin):
|
|
|
439
447
|
type: string
|
|
440
448
|
data:
|
|
441
449
|
type: string
|
|
450
|
+
is_shared:
|
|
451
|
+
type: boolean
|
|
442
452
|
source_id:
|
|
443
453
|
type: string
|
|
444
454
|
format: UUID
|
|
@@ -447,7 +457,7 @@ class NewAssetResource(Resource, ArgsMixin):
|
|
|
447
457
|
201:
|
|
448
458
|
description: New asset resource created
|
|
449
459
|
"""
|
|
450
|
-
(name, description, data, source_id) = self.get_arguments()
|
|
460
|
+
(name, description, data, is_shared, source_id) = self.get_arguments()
|
|
451
461
|
|
|
452
462
|
user_service.check_manager_project_access(project_id)
|
|
453
463
|
asset = assets_service.create_asset(
|
|
@@ -456,6 +466,7 @@ class NewAssetResource(Resource, ArgsMixin):
|
|
|
456
466
|
name,
|
|
457
467
|
description,
|
|
458
468
|
data,
|
|
469
|
+
is_shared,
|
|
459
470
|
source_id,
|
|
460
471
|
created_by=persons_service.get_current_user()["id"],
|
|
461
472
|
)
|
|
@@ -471,6 +482,12 @@ class NewAssetResource(Resource, ArgsMixin):
|
|
|
471
482
|
},
|
|
472
483
|
"description",
|
|
473
484
|
("data", {}, False, dict),
|
|
485
|
+
(
|
|
486
|
+
"is_shared",
|
|
487
|
+
True,
|
|
488
|
+
False,
|
|
489
|
+
inputs.boolean,
|
|
490
|
+
),
|
|
474
491
|
"episode_id",
|
|
475
492
|
]
|
|
476
493
|
)
|
|
@@ -479,6 +496,7 @@ class NewAssetResource(Resource, ArgsMixin):
|
|
|
479
496
|
args["name"],
|
|
480
497
|
args.get("description", ""),
|
|
481
498
|
args["data"],
|
|
499
|
+
args["is_shared"],
|
|
482
500
|
args["episode_id"],
|
|
483
501
|
)
|
|
484
502
|
|
|
@@ -658,3 +676,222 @@ class AssetAssetInstancesResource(Resource, ArgsMixin):
|
|
|
658
676
|
asset_id, args["asset_to_instantiate_id"], args["description"]
|
|
659
677
|
)
|
|
660
678
|
return asset_instance, 201
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
class BaseSetSharedAssetsResource(Resource, ArgsMixin):
|
|
682
|
+
|
|
683
|
+
@jwt_required()
|
|
684
|
+
def post(self, project_id=None, asset_type_id=None, asset_ids=None):
|
|
685
|
+
args = self.get_args(
|
|
686
|
+
[
|
|
687
|
+
(
|
|
688
|
+
"is_shared",
|
|
689
|
+
True,
|
|
690
|
+
False,
|
|
691
|
+
inputs.boolean,
|
|
692
|
+
),
|
|
693
|
+
]
|
|
694
|
+
)
|
|
695
|
+
return assets_service.set_shared_assets(
|
|
696
|
+
is_shared=args["is_shared"],
|
|
697
|
+
project_id=project_id,
|
|
698
|
+
asset_type_id=asset_type_id,
|
|
699
|
+
asset_ids=asset_ids,
|
|
700
|
+
)
|
|
701
|
+
|
|
702
|
+
|
|
703
|
+
class SetSharedProjectAssetsResource(BaseSetSharedAssetsResource):
|
|
704
|
+
"""
|
|
705
|
+
Share or unshare all assets (or a list of assets) for given project.
|
|
706
|
+
"""
|
|
707
|
+
|
|
708
|
+
@jwt_required()
|
|
709
|
+
def post(self, project_id):
|
|
710
|
+
"""
|
|
711
|
+
Share or unshare all assets (or a list of assets) for given project.
|
|
712
|
+
---
|
|
713
|
+
tags:
|
|
714
|
+
- Assets
|
|
715
|
+
consumes:
|
|
716
|
+
- multipart/form-data
|
|
717
|
+
parameters:
|
|
718
|
+
- in: path
|
|
719
|
+
name: project_id
|
|
720
|
+
required: True
|
|
721
|
+
type: string
|
|
722
|
+
format: UUID
|
|
723
|
+
x-example: a24a6ea4-ce75-4665-a070-57453082c25
|
|
724
|
+
- in: formData
|
|
725
|
+
name: asset_ids
|
|
726
|
+
default: None,
|
|
727
|
+
type: array
|
|
728
|
+
items:
|
|
729
|
+
type: UUID
|
|
730
|
+
x-example: ["a24a6ea4-ce75-4665-a070-57453082c25"]
|
|
731
|
+
- in: formData
|
|
732
|
+
name: is_shared
|
|
733
|
+
default: true
|
|
734
|
+
type: boolean
|
|
735
|
+
x-example: true
|
|
736
|
+
responses:
|
|
737
|
+
201:
|
|
738
|
+
description: All assets modified.
|
|
739
|
+
"""
|
|
740
|
+
args = self.get_args(
|
|
741
|
+
[
|
|
742
|
+
(
|
|
743
|
+
"asset_ids",
|
|
744
|
+
None,
|
|
745
|
+
False,
|
|
746
|
+
str,
|
|
747
|
+
"append",
|
|
748
|
+
),
|
|
749
|
+
]
|
|
750
|
+
)
|
|
751
|
+
user_service.check_manager_project_access(project_id)
|
|
752
|
+
return super().post(project_id=project_id, asset_ids=args["asset_ids"])
|
|
753
|
+
|
|
754
|
+
|
|
755
|
+
class SetSharedProjectAssetTypeAssetsResource(BaseSetSharedAssetsResource):
|
|
756
|
+
"""
|
|
757
|
+
Share or unshare all assets for given project and asset type.
|
|
758
|
+
"""
|
|
759
|
+
|
|
760
|
+
@jwt_required()
|
|
761
|
+
def post(self, project_id, asset_type_id):
|
|
762
|
+
"""
|
|
763
|
+
Share or unshare all assets for given project and asset type.
|
|
764
|
+
---
|
|
765
|
+
tags:
|
|
766
|
+
- Assets
|
|
767
|
+
consumes:
|
|
768
|
+
- multipart/form-data
|
|
769
|
+
parameters:
|
|
770
|
+
- in: path
|
|
771
|
+
name: project_id
|
|
772
|
+
required: True
|
|
773
|
+
type: string
|
|
774
|
+
format: UUID
|
|
775
|
+
x-example: a24a6ea4-ce75-4665-a070-57453082c25
|
|
776
|
+
- in: path
|
|
777
|
+
name: asset_type_id
|
|
778
|
+
required: True
|
|
779
|
+
type: string
|
|
780
|
+
format: UUID
|
|
781
|
+
x-example: a24a6ea4-ce75-4665-a070-57453082c25
|
|
782
|
+
- in: formData
|
|
783
|
+
name: is_shared
|
|
784
|
+
default: true
|
|
785
|
+
type: boolean
|
|
786
|
+
x-example: true
|
|
787
|
+
responses:
|
|
788
|
+
201:
|
|
789
|
+
description: All assets modified.
|
|
790
|
+
"""
|
|
791
|
+
user_service.check_manager_project_access(project_id)
|
|
792
|
+
return super().post(project_id=project_id, asset_type_id=asset_type_id)
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
class SetSharedAssetsResource(BaseSetSharedAssetsResource):
|
|
796
|
+
"""
|
|
797
|
+
Share or unshare all assets (or a list of assets) for given project.
|
|
798
|
+
"""
|
|
799
|
+
|
|
800
|
+
@jwt_required()
|
|
801
|
+
def post(self):
|
|
802
|
+
"""
|
|
803
|
+
Share or unshare a list of assets.
|
|
804
|
+
---
|
|
805
|
+
tags:
|
|
806
|
+
- Assets
|
|
807
|
+
consumes:
|
|
808
|
+
- multipart/form-data
|
|
809
|
+
parameters:
|
|
810
|
+
- in: formData
|
|
811
|
+
name: asset_ids
|
|
812
|
+
default: None,
|
|
813
|
+
type: array
|
|
814
|
+
items:
|
|
815
|
+
type: UUID
|
|
816
|
+
x-example: ["a24a6ea4-ce75-4665-a070-57453082c25"]
|
|
817
|
+
- in: formData
|
|
818
|
+
name: is_shared
|
|
819
|
+
default: true
|
|
820
|
+
type: boolean
|
|
821
|
+
x-example: true
|
|
822
|
+
responses:
|
|
823
|
+
201:
|
|
824
|
+
description: All assets modified.
|
|
825
|
+
"""
|
|
826
|
+
args = self.get_args(
|
|
827
|
+
[
|
|
828
|
+
(
|
|
829
|
+
"asset_ids",
|
|
830
|
+
[],
|
|
831
|
+
True,
|
|
832
|
+
str,
|
|
833
|
+
"append",
|
|
834
|
+
),
|
|
835
|
+
]
|
|
836
|
+
)
|
|
837
|
+
asset_ids = args["asset_ids"]
|
|
838
|
+
project_ids = set()
|
|
839
|
+
for asset_id in asset_ids:
|
|
840
|
+
project_ids.add(assets_service.get_asset(asset_id)["project_id"])
|
|
841
|
+
for project_id in project_ids:
|
|
842
|
+
user_service.check_manager_project_access(project_id)
|
|
843
|
+
return super().post(asset_ids=asset_ids)
|
|
844
|
+
|
|
845
|
+
|
|
846
|
+
class ProjectAssetsSharedUsedResource(Resource):
|
|
847
|
+
@jwt_required()
|
|
848
|
+
def get(self, project_id):
|
|
849
|
+
"""
|
|
850
|
+
Retrieve all shared assets used in project.
|
|
851
|
+
---
|
|
852
|
+
tags:
|
|
853
|
+
- Assets
|
|
854
|
+
parameters:
|
|
855
|
+
- in: path
|
|
856
|
+
name: project_id
|
|
857
|
+
required: True
|
|
858
|
+
type: string
|
|
859
|
+
format: UUID
|
|
860
|
+
x-example: a24a6ea4-ce75-4665-a070-57453082c25
|
|
861
|
+
responses:
|
|
862
|
+
200:
|
|
863
|
+
description: All shared assets used in project
|
|
864
|
+
"""
|
|
865
|
+
user_service.check_project_access(project_id)
|
|
866
|
+
return assets_service.get_shared_assets_used_in_project(project_id)
|
|
867
|
+
|
|
868
|
+
|
|
869
|
+
class ProjectEpisodeAssetsSharedUsedResource(Resource):
|
|
870
|
+
@jwt_required()
|
|
871
|
+
def get(self, project_id, episode_id):
|
|
872
|
+
"""
|
|
873
|
+
Retrieve all shared assets used in project episode.
|
|
874
|
+
---
|
|
875
|
+
tags:
|
|
876
|
+
- Assets
|
|
877
|
+
parameters:
|
|
878
|
+
- in: path
|
|
879
|
+
name: project_id
|
|
880
|
+
required: True
|
|
881
|
+
type: string
|
|
882
|
+
format: UUID
|
|
883
|
+
x-example: a24a6ea4-ce75-4665-a070-57453082c25
|
|
884
|
+
- in: path
|
|
885
|
+
name: episode_id
|
|
886
|
+
required: True
|
|
887
|
+
type: string
|
|
888
|
+
format: UUID
|
|
889
|
+
x-example: a24a6ea4-ce75-4665-a070-57453082c25
|
|
890
|
+
responses:
|
|
891
|
+
200:
|
|
892
|
+
description: All shared assets used in project episode
|
|
893
|
+
"""
|
|
894
|
+
user_service.check_project_access(project_id)
|
|
895
|
+
return assets_service.get_shared_assets_used_in_project(
|
|
896
|
+
project_id, episode_id
|
|
897
|
+
)
|
|
@@ -13,6 +13,8 @@ from zou.app.blueprints.auth.resources import (
|
|
|
13
13
|
RegistrationResource,
|
|
14
14
|
ResetPasswordResource,
|
|
15
15
|
TOTPResource,
|
|
16
|
+
SAMLSSOResource,
|
|
17
|
+
SAMLLoginResource,
|
|
16
18
|
)
|
|
17
19
|
|
|
18
20
|
routes = [
|
|
@@ -27,6 +29,8 @@ routes = [
|
|
|
27
29
|
("/auth/email-otp", EmailOTPResource),
|
|
28
30
|
("/auth/recovery-codes", RecoveryCodesResource),
|
|
29
31
|
("/auth/fido", FIDOResource),
|
|
32
|
+
("/auth/saml/sso", SAMLSSOResource),
|
|
33
|
+
("/auth/saml/login", SAMLLoginResource),
|
|
30
34
|
]
|
|
31
35
|
|
|
32
36
|
blueprint = Blueprint("auth", "auth")
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import datetime
|
|
2
1
|
import urllib.parse
|
|
3
2
|
|
|
4
|
-
from flask import request, jsonify, current_app
|
|
3
|
+
from flask import request, jsonify, current_app, redirect, make_response
|
|
5
4
|
from flask_restful import Resource
|
|
6
5
|
from flask_principal import (
|
|
7
6
|
Identity,
|
|
@@ -15,15 +14,17 @@ from flask_jwt_extended import (
|
|
|
15
14
|
set_access_cookies,
|
|
16
15
|
set_refresh_cookies,
|
|
17
16
|
unset_jwt_cookies,
|
|
17
|
+
unset_refresh_cookies,
|
|
18
18
|
get_jwt,
|
|
19
19
|
)
|
|
20
20
|
|
|
21
21
|
from sqlalchemy.exc import OperationalError, TimeoutError
|
|
22
22
|
from babel.dates import format_datetime
|
|
23
|
+
from saml2 import entity
|
|
23
24
|
|
|
24
25
|
from zou.app import app, config
|
|
25
26
|
from zou.app.mixin import ArgsMixin
|
|
26
|
-
from zou.app.utils import auth, emails, permissions
|
|
27
|
+
from zou.app.utils import auth, emails, permissions, date_helpers
|
|
27
28
|
from zou.app.services import (
|
|
28
29
|
persons_service,
|
|
29
30
|
auth_service,
|
|
@@ -78,7 +79,9 @@ class AuthenticatedResource(Resource):
|
|
|
78
79
|
description: Person not found
|
|
79
80
|
"""
|
|
80
81
|
person = persons_service.get_current_user(relations=True)
|
|
81
|
-
organisation = persons_service.get_organisation(
|
|
82
|
+
organisation = persons_service.get_organisation(
|
|
83
|
+
sensitive=permissions.has_admin_permissions()
|
|
84
|
+
)
|
|
82
85
|
return {
|
|
83
86
|
"authenticated": True,
|
|
84
87
|
"user": person,
|
|
@@ -218,7 +221,6 @@ class LoginResource(Resource, ArgsMixin):
|
|
|
218
221
|
"identity_type": "person",
|
|
219
222
|
},
|
|
220
223
|
)
|
|
221
|
-
auth_service.register_tokens(app, access_token, refresh_token)
|
|
222
224
|
identity_changed.send(
|
|
223
225
|
current_app._get_current_object(),
|
|
224
226
|
identity=Identity(user["id"], "person"),
|
|
@@ -228,15 +230,21 @@ class LoginResource(Resource, ArgsMixin):
|
|
|
228
230
|
"HTTP_X_REAL_IP", request.remote_addr
|
|
229
231
|
)
|
|
230
232
|
|
|
233
|
+
organisation = persons_service.get_organisation(
|
|
234
|
+
sensitive=user["role"] != "admin"
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
response = jsonify(
|
|
238
|
+
{
|
|
239
|
+
"user": user,
|
|
240
|
+
"organisation": organisation,
|
|
241
|
+
"login": True,
|
|
242
|
+
"access_token": access_token,
|
|
243
|
+
"refresh_token": refresh_token,
|
|
244
|
+
}
|
|
245
|
+
)
|
|
246
|
+
|
|
231
247
|
if is_from_browser(request.user_agent):
|
|
232
|
-
organisation = persons_service.get_organisation()
|
|
233
|
-
response = jsonify(
|
|
234
|
-
{
|
|
235
|
-
"user": user,
|
|
236
|
-
"organisation": organisation,
|
|
237
|
-
"login": True,
|
|
238
|
-
}
|
|
239
|
-
)
|
|
240
248
|
set_access_cookies(response, access_token)
|
|
241
249
|
set_refresh_cookies(response, refresh_token)
|
|
242
250
|
events_service.create_login_log(user["id"], ip_address, "web")
|
|
@@ -244,12 +252,6 @@ class LoginResource(Resource, ArgsMixin):
|
|
|
244
252
|
events_service.create_login_log(
|
|
245
253
|
user["id"], ip_address, "script"
|
|
246
254
|
)
|
|
247
|
-
response = {
|
|
248
|
-
"login": True,
|
|
249
|
-
"user": user,
|
|
250
|
-
"access_token": access_token,
|
|
251
|
-
"refresh_token": refresh_token,
|
|
252
|
-
}
|
|
253
255
|
current_app.logger.info(f"User {email} is logged in.")
|
|
254
256
|
return response
|
|
255
257
|
except WrongUserException:
|
|
@@ -384,10 +386,10 @@ class RefreshTokenResource(Resource):
|
|
|
384
386
|
"identity_type": "person",
|
|
385
387
|
},
|
|
386
388
|
)
|
|
387
|
-
auth_service.register_tokens(app, access_token)
|
|
388
389
|
if is_from_browser(request.user_agent):
|
|
389
390
|
response = jsonify({"refresh": True})
|
|
390
391
|
set_access_cookies(response, access_token)
|
|
392
|
+
unset_refresh_cookies(response)
|
|
391
393
|
else:
|
|
392
394
|
return {"access_token": access_token}
|
|
393
395
|
|
|
@@ -561,7 +563,7 @@ class ChangePasswordResource(Resource, ArgsMixin):
|
|
|
561
563
|
)
|
|
562
564
|
organisation = persons_service.get_organisation()
|
|
563
565
|
time_string = format_datetime(
|
|
564
|
-
|
|
566
|
+
date_helpers.get_utc_now_datetime(),
|
|
565
567
|
tzinfo=user["timezone"],
|
|
566
568
|
locale=user["locale"],
|
|
567
569
|
)
|
|
@@ -734,6 +736,11 @@ class ResetPasswordResource(Resource, ArgsMixin):
|
|
|
734
736
|
|
|
735
737
|
try:
|
|
736
738
|
user = persons_service.get_person_by_email(args["email"])
|
|
739
|
+
if not user["active"]:
|
|
740
|
+
return (
|
|
741
|
+
{"error": True, "message": "This user is inactive."},
|
|
742
|
+
400,
|
|
743
|
+
)
|
|
737
744
|
except PersonNotFoundException:
|
|
738
745
|
return (
|
|
739
746
|
{"error": True, "message": "Email not listed in database."},
|
|
@@ -752,7 +759,7 @@ class ResetPasswordResource(Resource, ArgsMixin):
|
|
|
752
759
|
query,
|
|
753
760
|
)
|
|
754
761
|
time_string = format_datetime(
|
|
755
|
-
|
|
762
|
+
date_helpers.get_utc_now_datetime(),
|
|
756
763
|
tzinfo=user["timezone"],
|
|
757
764
|
locale=user["locale"],
|
|
758
765
|
)
|
|
@@ -1329,3 +1336,128 @@ class RecoveryCodesResource(Resource, ArgsMixin):
|
|
|
1329
1336
|
},
|
|
1330
1337
|
400,
|
|
1331
1338
|
)
|
|
1339
|
+
|
|
1340
|
+
|
|
1341
|
+
class SAMLSSOResource(Resource, ArgsMixin):
|
|
1342
|
+
"""
|
|
1343
|
+
Resource to allow a user to login with SAML SSO.
|
|
1344
|
+
"""
|
|
1345
|
+
|
|
1346
|
+
def post(self):
|
|
1347
|
+
"""
|
|
1348
|
+
Resource to allow a user to login with SAML SSO.
|
|
1349
|
+
---
|
|
1350
|
+
description: ""
|
|
1351
|
+
tags:
|
|
1352
|
+
- Authentication
|
|
1353
|
+
responses:
|
|
1354
|
+
302:
|
|
1355
|
+
description: Login successful, redirect to the home page.
|
|
1356
|
+
400:
|
|
1357
|
+
description: Wrong parameter
|
|
1358
|
+
"""
|
|
1359
|
+
if not config.SAML_ENABLED:
|
|
1360
|
+
return {"error": "SAML is not enabled."}, 400
|
|
1361
|
+
authn_response = current_app.extensions[
|
|
1362
|
+
"saml_client"
|
|
1363
|
+
].parse_authn_request_response(
|
|
1364
|
+
request.form["SAMLResponse"], entity.BINDING_HTTP_POST
|
|
1365
|
+
)
|
|
1366
|
+
authn_response.get_identity()
|
|
1367
|
+
email = authn_response.get_subject().text
|
|
1368
|
+
person_info = {
|
|
1369
|
+
k: (
|
|
1370
|
+
" ".join(v)
|
|
1371
|
+
if isinstance(v, list) and k in ["first_name", "last_name"]
|
|
1372
|
+
else v
|
|
1373
|
+
)
|
|
1374
|
+
for k, v in authn_response.ava.items()
|
|
1375
|
+
if k
|
|
1376
|
+
in [
|
|
1377
|
+
"first_name",
|
|
1378
|
+
"last_name",
|
|
1379
|
+
"phone",
|
|
1380
|
+
"role",
|
|
1381
|
+
"departments",
|
|
1382
|
+
"studio_id",
|
|
1383
|
+
"active",
|
|
1384
|
+
]
|
|
1385
|
+
}
|
|
1386
|
+
try:
|
|
1387
|
+
user = persons_service.get_person_by_email(email)
|
|
1388
|
+
for k, v in person_info.items():
|
|
1389
|
+
if user.get(k) != v:
|
|
1390
|
+
persons_service.update_person(
|
|
1391
|
+
user["id"], person_info, bypass_protected_accounts=True
|
|
1392
|
+
)
|
|
1393
|
+
break
|
|
1394
|
+
except PersonNotFoundException:
|
|
1395
|
+
user = persons_service.create_person(
|
|
1396
|
+
email, "default".encode("utf-8"), **person_info
|
|
1397
|
+
)
|
|
1398
|
+
|
|
1399
|
+
response = make_response(
|
|
1400
|
+
redirect(f"{config.DOMAIN_PROTOCOL}://{config.DOMAIN_NAME}")
|
|
1401
|
+
)
|
|
1402
|
+
|
|
1403
|
+
if user["active"]:
|
|
1404
|
+
access_token = create_access_token(
|
|
1405
|
+
identity=user["id"],
|
|
1406
|
+
additional_claims={
|
|
1407
|
+
"identity_type": "person",
|
|
1408
|
+
},
|
|
1409
|
+
)
|
|
1410
|
+
refresh_token = create_refresh_token(
|
|
1411
|
+
identity=user["id"],
|
|
1412
|
+
additional_claims={
|
|
1413
|
+
"identity_type": "person",
|
|
1414
|
+
},
|
|
1415
|
+
)
|
|
1416
|
+
identity_changed.send(
|
|
1417
|
+
current_app._get_current_object(),
|
|
1418
|
+
identity=Identity(user["id"], "person"),
|
|
1419
|
+
)
|
|
1420
|
+
|
|
1421
|
+
ip_address = request.environ.get(
|
|
1422
|
+
"HTTP_X_REAL_IP", request.remote_addr
|
|
1423
|
+
)
|
|
1424
|
+
|
|
1425
|
+
set_access_cookies(response, access_token)
|
|
1426
|
+
set_refresh_cookies(response, refresh_token)
|
|
1427
|
+
events_service.create_login_log(user["id"], ip_address, "web")
|
|
1428
|
+
|
|
1429
|
+
return response
|
|
1430
|
+
|
|
1431
|
+
|
|
1432
|
+
class SAMLLoginResource(Resource, ArgsMixin):
|
|
1433
|
+
"""
|
|
1434
|
+
Resource to allow a user to login with SAML SSO.
|
|
1435
|
+
"""
|
|
1436
|
+
|
|
1437
|
+
def get(self):
|
|
1438
|
+
"""
|
|
1439
|
+
Resource to allow a user to login with SAML SSO.
|
|
1440
|
+
---
|
|
1441
|
+
description: ""
|
|
1442
|
+
tags:
|
|
1443
|
+
- Authentication
|
|
1444
|
+
responses:
|
|
1445
|
+
302:
|
|
1446
|
+
description: Redirect to the SAML IDP.
|
|
1447
|
+
400:
|
|
1448
|
+
description: Wrong parameter.
|
|
1449
|
+
"""
|
|
1450
|
+
if not config.SAML_ENABLED:
|
|
1451
|
+
return {"error": "SAML is not enabled."}, 400
|
|
1452
|
+
_, info = current_app.extensions[
|
|
1453
|
+
"saml_client"
|
|
1454
|
+
].prepare_for_authenticate()
|
|
1455
|
+
|
|
1456
|
+
redirect_url = None
|
|
1457
|
+
|
|
1458
|
+
# Select the IdP URL to send the AuthN request to
|
|
1459
|
+
for key, value in info["headers"]:
|
|
1460
|
+
if key == "Location":
|
|
1461
|
+
redirect_url = value
|
|
1462
|
+
|
|
1463
|
+
return redirect(redirect_url, code=302)
|
|
@@ -398,14 +398,14 @@ class ProjectEntityLinksResource(Resource, ArgsMixin):
|
|
|
398
398
|
type: string
|
|
399
399
|
format: Number
|
|
400
400
|
x-example: 2
|
|
401
|
-
- in:
|
|
402
|
-
name:
|
|
401
|
+
- in: query
|
|
402
|
+
name: limit
|
|
403
403
|
required: False
|
|
404
404
|
type: string
|
|
405
405
|
format: Number
|
|
406
406
|
x-example: 100
|
|
407
|
-
- in:
|
|
408
|
-
name:
|
|
407
|
+
- in: query
|
|
408
|
+
name: cursor_created_at
|
|
409
409
|
required: False
|
|
410
410
|
type: string
|
|
411
411
|
format: Datetime
|