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.
Files changed (165) hide show
  1. zou/__init__.py +1 -1
  2. zou/app/__init__.py +10 -2
  3. zou/app/api.py +2 -0
  4. zou/app/blueprints/assets/__init__.py +22 -0
  5. zou/app/blueprints/assets/resources.py +241 -4
  6. zou/app/blueprints/auth/__init__.py +4 -0
  7. zou/app/blueprints/auth/resources.py +154 -22
  8. zou/app/blueprints/breakdown/resources.py +4 -4
  9. zou/app/blueprints/chats/__init__.py +22 -0
  10. zou/app/blueprints/chats/resources.py +199 -0
  11. zou/app/blueprints/comments/resources.py +36 -19
  12. zou/app/blueprints/crud/__init__.py +12 -0
  13. zou/app/blueprints/crud/attachment_file.py +14 -5
  14. zou/app/blueprints/crud/base.py +29 -28
  15. zou/app/blueprints/crud/chat.py +13 -0
  16. zou/app/blueprints/crud/chat_message.py +13 -0
  17. zou/app/blueprints/crud/comments.py +85 -29
  18. zou/app/blueprints/crud/custom_action.py +1 -1
  19. zou/app/blueprints/crud/day_off.py +47 -9
  20. zou/app/blueprints/crud/department.py +1 -25
  21. zou/app/blueprints/crud/entity.py +46 -5
  22. zou/app/blueprints/crud/entity_type.py +13 -1
  23. zou/app/blueprints/crud/event.py +1 -1
  24. zou/app/blueprints/crud/file_status.py +1 -1
  25. zou/app/blueprints/crud/metadata_descriptor.py +24 -10
  26. zou/app/blueprints/crud/organisation.py +22 -5
  27. zou/app/blueprints/crud/output_file.py +1 -1
  28. zou/app/blueprints/crud/output_type.py +1 -1
  29. zou/app/blueprints/crud/person.py +32 -24
  30. zou/app/blueprints/crud/playlist.py +1 -1
  31. zou/app/blueprints/crud/preview_background_file.py +6 -7
  32. zou/app/blueprints/crud/preview_file.py +1 -1
  33. zou/app/blueprints/crud/project.py +14 -6
  34. zou/app/blueprints/crud/project_status.py +1 -1
  35. zou/app/blueprints/crud/schedule_item.py +4 -2
  36. zou/app/blueprints/crud/software.py +1 -1
  37. zou/app/blueprints/crud/status_automation.py +1 -1
  38. zou/app/blueprints/crud/studio.py +33 -0
  39. zou/app/blueprints/crud/task.py +47 -3
  40. zou/app/blueprints/crud/task_status.py +1 -1
  41. zou/app/blueprints/crud/task_type.py +4 -4
  42. zou/app/blueprints/crud/working_file.py +4 -8
  43. zou/app/blueprints/events/resources.py +13 -12
  44. zou/app/blueprints/export/csv/assets.py +15 -6
  45. zou/app/blueprints/export/csv/edits.py +15 -5
  46. zou/app/blueprints/export/csv/playlists.py +1 -1
  47. zou/app/blueprints/export/csv/shots.py +15 -5
  48. zou/app/blueprints/export/csv/time_spents.py +1 -1
  49. zou/app/blueprints/files/resources.py +22 -23
  50. zou/app/blueprints/index/resources.py +38 -29
  51. zou/app/blueprints/news/resources.py +25 -11
  52. zou/app/blueprints/persons/__init__.py +5 -2
  53. zou/app/blueprints/persons/resources.py +126 -120
  54. zou/app/blueprints/previews/__init__.py +18 -8
  55. zou/app/blueprints/previews/resources.py +569 -328
  56. zou/app/blueprints/projects/resources.py +1 -1
  57. zou/app/blueprints/search/resources.py +18 -6
  58. zou/app/blueprints/shots/__init__.py +5 -0
  59. zou/app/blueprints/shots/resources.py +134 -4
  60. zou/app/blueprints/source/__init__.py +6 -6
  61. zou/app/blueprints/source/csv/assets.py +10 -3
  62. zou/app/blueprints/source/csv/base.py +1 -1
  63. zou/app/blueprints/source/csv/edits.py +10 -3
  64. zou/app/blueprints/source/csv/shots.py +10 -3
  65. zou/app/blueprints/source/{edl.py → otio.py} +84 -41
  66. zou/app/blueprints/tasks/__init__.py +3 -2
  67. zou/app/blueprints/tasks/resources.py +83 -52
  68. zou/app/blueprints/user/__init__.py +9 -0
  69. zou/app/blueprints/user/resources.py +170 -12
  70. zou/app/config.py +10 -0
  71. zou/app/mixin.py +6 -5
  72. zou/app/models/attachment_file.py +10 -4
  73. zou/app/models/base.py +18 -13
  74. zou/app/models/build_job.py +7 -4
  75. zou/app/models/chat.py +44 -0
  76. zou/app/models/chat_message.py +37 -0
  77. zou/app/models/comment.py +1 -0
  78. zou/app/models/day_off.py +3 -0
  79. zou/app/models/entity.py +4 -6
  80. zou/app/models/entity_type.py +2 -0
  81. zou/app/models/organisation.py +14 -15
  82. zou/app/models/person.py +6 -1
  83. zou/app/models/project.py +3 -0
  84. zou/app/models/search_filter.py +11 -0
  85. zou/app/models/search_filter_group.py +10 -0
  86. zou/app/models/serializer.py +17 -17
  87. zou/app/models/status_automation.py +2 -0
  88. zou/app/models/studio.py +13 -0
  89. zou/app/models/subscription.py +2 -2
  90. zou/app/models/task.py +6 -1
  91. zou/app/models/task_status.py +1 -0
  92. zou/app/models/task_type.py +1 -0
  93. zou/app/models/working_file.py +1 -1
  94. zou/app/services/assets_service.py +101 -14
  95. zou/app/services/auth_service.py +17 -44
  96. zou/app/services/breakdown_service.py +37 -5
  97. zou/app/services/chats_service.py +279 -0
  98. zou/app/services/comments_service.py +110 -65
  99. zou/app/services/concepts_service.py +4 -12
  100. zou/app/services/deletion_service.py +43 -30
  101. zou/app/services/edits_service.py +5 -11
  102. zou/app/services/emails_service.py +4 -4
  103. zou/app/services/entities_service.py +17 -2
  104. zou/app/services/events_service.py +12 -4
  105. zou/app/services/exception.py +5 -5
  106. zou/app/services/names_service.py +7 -2
  107. zou/app/services/news_service.py +17 -9
  108. zou/app/services/persons_service.py +38 -21
  109. zou/app/services/playlists_service.py +8 -7
  110. zou/app/services/preview_files_service.py +137 -10
  111. zou/app/services/projects_service.py +5 -14
  112. zou/app/services/shots_service.py +221 -49
  113. zou/app/services/sync_service.py +46 -42
  114. zou/app/services/tasks_service.py +185 -46
  115. zou/app/services/time_spents_service.py +67 -20
  116. zou/app/services/user_service.py +350 -107
  117. zou/app/stores/auth_tokens_store.py +2 -1
  118. zou/app/stores/file_store.py +18 -0
  119. zou/app/stores/publisher_store.py +7 -7
  120. zou/app/stores/queue_store.py +1 -0
  121. zou/app/swagger.py +36 -20
  122. zou/app/utils/cache.py +2 -0
  123. zou/app/utils/commands.py +104 -7
  124. zou/app/utils/csv_utils.py +1 -4
  125. zou/app/utils/date_helpers.py +33 -17
  126. zou/app/utils/dbhelpers.py +14 -1
  127. zou/app/utils/emails.py +2 -2
  128. zou/app/utils/fido.py +22 -0
  129. zou/app/utils/flask.py +1 -0
  130. zou/app/utils/query.py +54 -6
  131. zou/app/utils/redis.py +11 -0
  132. zou/app/utils/saml.py +51 -0
  133. zou/app/utils/string.py +2 -0
  134. zou/app/utils/thumbnail.py +4 -2
  135. zou/cli.py +76 -18
  136. zou/debug.py +4 -2
  137. zou/event_stream.py +122 -165
  138. zou/job_settings.py +1 -0
  139. zou/migrations/env.py +0 -0
  140. zou/migrations/utils/base.py +6 -6
  141. zou/migrations/versions/1bb55759146f_add_table_studio.py +67 -0
  142. zou/migrations/versions/1fab8c420678_add_attachments_to_message_chats.py +56 -0
  143. zou/migrations/versions/23122f290ca2_add_entity_chat_models.py +149 -0
  144. zou/migrations/versions/32f134ff1201_add_is_shared_flag_to_filters.py +33 -0
  145. zou/migrations/versions/57222395f2be_add_statusautomation_import_last_revision.py +41 -0
  146. zou/migrations/versions/59a7445a966c_add_entity_is_shared.py +41 -0
  147. zou/migrations/versions/5b980f0dc365_add_comment_links.py +35 -0
  148. zou/migrations/versions/680c64565f9d_for_searchfiltergroup_is_shared.py +35 -0
  149. zou/migrations/versions/8e67c183bed7_add_preference_fields.py +71 -0
  150. zou/migrations/versions/92b40d79ad3f_allow_message_attachments.py +38 -0
  151. zou/migrations/versions/971dbf5a0faf_add_short_name_for_asset_type_entity_.py +33 -0
  152. zou/migrations/versions/9b85c14fa8a7_add_day_off_new_columns.py +68 -0
  153. zou/migrations/versions/9d3bb33c6fc6_add_department_keys_to_filter_models.py +73 -0
  154. zou/migrations/versions/a252a094e977_add_descriptions_for_entities_tasks_and_.py +40 -0
  155. zou/migrations/versions/be56dc0fb760_for_is_shared_disallow_nullable.py +102 -0
  156. zou/migrations/versions/ca28796a2a62_add_is_done_field_to_the_task_model.py +108 -0
  157. zou/migrations/versions/f344b867a911_for_description_of_entity_task_working_.py +75 -0
  158. zou/remote/config_payload.py +2 -1
  159. zou/utils/movie.py +14 -4
  160. {zou-0.19.14.dist-info → zou-0.20.11.dist-info}/METADATA +75 -69
  161. {zou-0.19.14.dist-info → zou-0.20.11.dist-info}/RECORD +164 -135
  162. {zou-0.19.14.dist-info → zou-0.20.11.dist-info}/WHEEL +1 -1
  163. {zou-0.19.14.dist-info → zou-0.20.11.dist-info}/LICENSE +0 -0
  164. {zou-0.19.14.dist-info → zou-0.20.11.dist-info}/entry_points.txt +0 -0
  165. {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.19.14"
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 not config.DEBUG:
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
- return user_service.check_project_access(project_id)
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(criterions)
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
- datetime.datetime.utcnow(),
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
- datetime.datetime.utcnow(),
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: limit
402
- name: page
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: cursor_created_at
408
- name: page
407
+ - in: query
408
+ name: cursor_created_at
409
409
  required: False
410
410
  type: string
411
411
  format: Datetime