def delete(self, id=None, override_permissions=False): """To delete a specified entity. Must own entity by default. See query(). """ if id is None: # Somebody called DELETE /api/<collection> which we don't support. self.error(405) self.response.headers['Allow'] = 'GET, HEAD, POST' return user = self.get_current_user() id = self.model.get_long_uid(id) # Checking override first may save db time. if override_permissions or owns(user, id): entity = self.model.get_by_id(id) if isinstance(entity, DatastoreModel): entity.deleted = True entity.put() elif isinstance(entity, SqlModel): self.model.delete_multi([entity]) # @todo: this seems logical, but doesn't deal with other users who # also own this thing. Don't want to implement until there's a # general solution. # p = user.get_owner_property(entity) # if p: # p.remove(entity.uid) # user.put() self.http_no_content() return entity else: self.error(403)
def get(self, id=None, override_permissions=False): """Get a list of entities or a particular entity. Args: override_permissions: (bool) default False, set to True to remove permission restrictions from ONLY id-based gets. See query(). """ if self.allowed_by_jwt: logging.info( "RestHandler overriding normal permission b/c this endpoint " "is explicitly allowed by the jwt." ) override_permissions = True if not id: # For /api/<collection>, returns a list. return self.query(override_permissions=override_permissions) # For /api/<collection>/<id>, returns an object, strongly consistent, # or 403 or 404. result = self.model.get_by_id(id) if not result: self.error(404) return ok = override_permissions or owns(self.get_current_user(), result) if not ok: self.error(403) return self.write(result) return result
def get(self, parent_type, rel_id): # You must be a super admin or own the related object. user = self.get_current_user() rel_id = self.get_long_uid(parent_type, rel_id) if not rel_id: return self.http_not_found() if not owns(user, rel_id): return self.http_forbidden() # Simulate a query string parameter so existing handler code # can run the query. self.request.GET[relationship_property] = rel_id # Of all the kinds we can query with RelatedQuery (Classroom, # Surveys, Reports) only clasrooms have a name for ordering. ordered_types = (Classroom, ) if model in ordered_types and 'order' not in self.request.GET: self.request.GET['order'] = 'name' # There is no id-based GET for these RelatedQuery endpoints, # e.g. we don't support /api/projects/X/users/Y. # Skip right to the inherited query() method. return super(RelatedQueryHandler, self).query(override_permissions=True)
def get_team_reports(self, team_id): user = self.get_current_user() team = Team.get_by_id(team_id) if not team: return self.http_not_found() if not owns(user, team) and not has_captain_permission(user, team): return self.http_forbidden("Only team members can list reports.") # Limit access to preview reports, super admin only. show_preview_reports = True if user.super_admin else False # Returns both team- and class-level reports. all_reports = Report.get_for_team(team.uid, show_preview_reports) # Any team member can see the team reports... allowed_reports = [r for r in all_reports if not r.classroom_id] # ...but limit access to classroom reports. classroom_reports = [r for r in all_reports if r.classroom_id] classroom_ids = [r.classroom_id for r in classroom_reports] classrooms = {c.uid: c for c in Classroom.get_by_id(classroom_ids)} for report in classroom_reports: if has_contact_permission(user, classrooms[report.classroom_id]): allowed_reports.append(report) # Add a custom link for users to access each report. self.write([ dict(r.to_client_dict(), link=self.report_link(r)) for r in allowed_reports ])
def put(self, id=None): if id is None: # Somebody called PUT /api/<collection> which we don't support. return self.http_method_not_allowed('GET, HEAD, POST') id = self.model.get_long_uid(id) if not owns(self.get_current_user(), id): return self.http_forbidden() force = self.request.get('force', None) == 'true' # from query str params = self.get_params(self.model.property_types()) # json body # Don't allow any changes to response privacy. params.pop('private', None) try: entity = Response.update_or_conflict(id, params, force) except ResponseNotFound: return self.http_not_found() except JsonTextValueLengthError: return self.http_payload_too_large("Value too long.") except JsonTextDictLengthError: return self.http_payload_too_large("Body has too many keys.") except ResponseBodyKeyConflict as e: return self.http_conflict({ 'message': ("Keys conflict: {}. Repeat with ?force=true to override.". format(', '.join(e.args[0]))), 'keys': e.args[0], }) self.write(entity)
def post(self): # Anyone is allowed to post responses. params = self.get_params(Response.property_types()) user = self.get_current_user() if 'user_id' not in params: params['user_id'] = user.uid required_params = ('team_id', 'parent_id', 'module_label') for k in required_params: if not params.get(k, None): return self.http_bad_request("{} required.".format(k)) # Validate the parent, if it's a cycle. parent_id = params.get('parent_id', None) parent_kind = SqlModel.get_kind(parent_id) if parent_kind == 'Cycle': cycle = SqlModel.kind_to_class(parent_kind).get_by_id(parent_id) if not cycle or not owns(user, parent_id): return self.http_forbidden("Must own the parent to respond.") # ...else this is a step label, so allow any truthy string. # Permission to create varies by type/level. params['type'] = params.get('type', Response.USER_LEVEL_SYMBOL) if params['type'] == Response.TEAM_LEVEL_SYMBOL: if not owns(user, params['team_id']): return self.http_forbidden("Must own the team to respond.") elif not owns(user, params['user_id']): return self.http_forbidden("May not create responses for others.") try: new_entity = Response.insert_or_conflict(params) except ResponseIndexConflict: return self.http_conflict( "Response for this type-user-team-parent-module combination " "exists. Send a request like `PUT /api/responses/:id`.") except JsonTextValueLengthError: return self.http_payload_too_large("Value too long.") except JsonTextDictLengthError: return self.http_payload_too_large("Body has too many keys.") self.write(new_entity)
def post(self, id): if not owns(self.get_current_user(), id): return self.http_forbidden("You don't own that network.") net = Network.get_by_id(id) if not net: return self.http_not_found() net.code = Network.generate_unique_code() net.put() self.write(net)
def get(self, user_id): user = self.get_current_user() program_label = self.request.GET.pop('program', None) if program_label: program = Program.get_by_label(program_label) if not program: self.write([]) return program_id = program.uid else: program_id = None if not owns(user, user_id): return self.http_forbidden("Can't list networks for someone else.") self.write(Network.query_by_user(user, program_id=program_id))
def get(self, parent_type, rel_id): user = self.get_current_user() team = Team.get_by_id(rel_id) if not team: return self.http_not_found() if not owns(user, team) and not has_captain_permission(user, team): return self.http_forbidden("Only team members can get responses.") parent_id = self.get_param('parent_id', str, None) # We return empty dictionaries for the `body` property of some # responses (private responses belonging to other users). responses = Response.get_for_teams(user, [team.uid], parent_id) self.write(responses)
def get_network_reports(self, network_id): user = self.get_current_user() net = Network.get_by_id(network_id) if not net: return self.http_not_found() if not owns(user, net): return self.http_forbidden("Only net admins can list reports.") # Limit access to preview reports, super admin only. show_preview_reports = True if user.super_admin else False all_reports = Report.get_for_network(net.uid, show_preview_reports) # Add a custom link for users to access each report. self.write([ dict(r.to_client_dict(), link=self.report_link(r)) for r in all_reports ])
def get_organization_reports(self, organization_id): user = self.get_current_user() org = Organization.get_by_id(organization_id) if not org: return self.http_not_found() if not owns(user, org): return self.http_forbidden("Only org admins can list reports.") # Limit access to preview reports, super admin only. show_preview_reports = True if user.super_admin else False all_reports = Report.get_for_organization(org.uid, show_preview_reports) # Add a custom link for users to access each report. self.write([ dict(r.to_client_dict(), link=self.report_link(r)) for r in all_reports ])
def put(self, id=None, override_permissions=False): """To modify a specified entity. Must own entity by default. See query(). """ if id is None: # Somebody called PUT /api/<collection> which we don't support. self.error(405) self.response.headers['Allow'] = 'GET, HEAD, POST' return id = self.model.get_long_uid(id) # Checking override first may save db time. if override_permissions or owns(self.get_current_user(), id): params = self.get_params(self.model.property_types()) entity = self.model.get_by_id(id) for k, v in params.items(): setattr(entity, k, v) entity.put() self.write(entity) return entity else: self.error(403)
def get(self, org_id): user = self.get_current_user() org = Organization.get_by_id(org_id) if not org: return self.http_not_found() if not owns(user, org): return self.http_forbidden() teams = Team.query_by_organization(org_id) team_ids = [t.uid for t in teams] classrooms = Classroom.query_by_teams(team_ids) cycles = Cycle.query_by_teams(team_ids) responses = Response.get_for_teams(user, team_ids) users = User.query_by_team(team_ids) self.write({ 'classrooms': [e.to_client_dict() for e in classrooms], 'cycles': [e.to_client_dict() for e in cycles], 'teams': [e.to_client_dict() for e in teams], 'responses': [e.to_client_dict() for e in responses], 'users': [e.to_client_dict() for e in users], })
def get_permissions(self, id, token): """Returns tuple like (dataset, http_error_method_name, error_msg).""" if token: # This URL has jwt authentication embedded in the query string so # that it is shareable. Ignore other permission rules as long as # the jwt is valid. payload, error = jwt_helper.decode(token) if not payload or error: return (None, 'http_unauthorized', error) allowed_endpoints = payload.get('allowed_endpoints', []) if self.get_endpoint_str() not in allowed_endpoints: return (None, 'http_forbidden', "Endpoint not allowed.") ds = Dataset.get_by_id(id) if not ds: return (None, 'http_not_found', None) else: # Without a token, do normal user-based authentication. user = self.get_current_user() if user.user_type == 'public': return (None, 'http_unauthorized', '') ds = Dataset.get_by_id(id) if not ds: return (None, 'http_not_found', None) if not ds.parent_id: if not user.super_admin: return (None, 'http_forbidden', "Only supers can get parentless datasets.") elif not owns(self.get_current_user(), ds.parent_id): return (None, 'http_forbidden', "Must own parent.") return (ds, None, None)
def get(self, user_id=None, team_id=None): complete = False # Determine authenticated user based on JWT token # @todo: can we apply jti or some other rule to make sure this URL isn't # inappropriately shareable? token = self.request.get('token', None) payload, error = jwt_helper.decode(token) if not payload or error: return self.http_forbidden() auth_user = User.get_by_id(payload['user_id']) user = User.get_by_id(user_id) team = Team.get_by_id(team_id) if not user or not team: return self.http_not_found() # The authenticated user can only retrieve their own certificate. # The authenticated user must own the team that they are requesting the # certificate for. if not auth_user == user and not owns(auth_user, team): return self.http_forbidden() classrooms = Classroom.get( contact_id=user.uid, team_id=team_id, ) cycles = Cycle.get( team_id=team_id, order='ordinal', ) if len(classrooms) > 0 and len(cycles) > 0: cycle_participation = self.get_cycle_participation_pct( cycles, classrooms, ) participation_complete = self.has_completed_three_cycles( cycle_participation) else: cycle_participation = [{ 'ordinal': c.ordinal, 'pct': 0, } for c in cycles] participation_complete = False exit_survey_complete = self.has_completed_exit_survey( user, team_id, ) if (exit_survey_complete and participation_complete): complete = True if (complete): # If a teacher has successfully completed participation for three # cycles, the certificate should not show any incomplete cycles # because they aren't relevant for the requirement of receiving the # completion certificate. See #1223. cycles_to_display = [ c for c in cycle_participation if c['pct'] >= 80 ][0:3] else: cycles_to_display = cycle_participation if util.is_localhost(): neptune_protocol = 'http' neptune_domain = 'localhost:8080' else: neptune_protocol = 'https' neptune_domain = os.environ['NEPTUNE_DOMAIN'] self.write( 'completion.html', neptune_protocol=neptune_protocol, neptune_domain=neptune_domain, complete=complete, user_to_display=user, team=team, cycles_to_display=cycles_to_display, exit_survey_complete=exit_survey_complete, )