When executing the courses.courseWork.studentSubmissions.patch method in the Google Classroom API, a 403 error is returned when I try to update the student's submission. Below is my code.
from googleapiclient.discovery import build
from oauth2client import client
import simplejson as json
class Google:
SCOPE = {
"profile": {"scope": "profile email", "access_type": "offline"},
"classroom": {"scope": 'https://www.googleapis.com/auth/classroom.courses.readonly '
'https://www.googleapis.com/auth/classroom.rosters.readonly '
'https://www.googleapis.com/auth/classroom.profile.emails '
'https://www.googleapis.com/auth/classroom.profile.photos ',
"access_type": "offline"},
"classwork":{
"scope": "https://www.googleapis.com/auth/classroom.coursework.students https://www.googleapis.com/auth/classroom.coursework.me",
"access_type":"offline"
},
"submission":{
"scope": "https://www.googleapis.com/auth/classroom.coursework.me https://www.googleapis.com/auth/classroom.coursework.students profile email",
"access_type":"offline"
}
}
ERRORS = {
"invalid_request":{"code":"invalid_request","msg":"Invalid request. Please Try with login again."},
"account_used":{"code":"account_used","msg":"Google account is already configured with different PracTutor Account."},
"assignment_permission_denied":{"code":"assignment_permission_denied","msg":"permission denied"},
"unknown_error":{"code":"unknown_error","msg":"something went wrong."}
}
def __init__(self, code = "", genFor = "profile"):
if code:
genFor = genFor if genFor else "profile"
self.credentials = client.credentials_from_clientsecrets_and_code(pConfig['googleOauthSecretFile'],self.SCOPE[genFor]["scope"], code)
self.http_auth = self.credentials.authorize(httplib2.Http())
cred_json = self.credentials.to_json()
idinfo = json.loads(cred_json)["id_token"]
else:
raise ValueError(Google.ERRORS["invalid_request"])
def getUserInfo(self):
service = build(serviceName='oauth2', version='v2', http=self.http_auth)
idinfo = service.userinfo().get().execute()
return idinfo
def getClasses(self):
courses = []
page_token = None
service = build('classroom', 'v1', http=self.http_auth)
while True:
response = service.courses().list(teacherId="me",pageToken=page_token,
pageSize=100).execute()
courses.extend(response.get('courses', []))
page_token = response.get('nextPageToken', None)
if not page_token:
break
return courses
def getStudent(self,course_id):
students = []
page_token = None
service = build('classroom', 'v1', http=self.http_auth)
while True:
response = service.courses().students().list(courseId=course_id, pageToken=page_token,
pageSize=100).execute()
students.extend(response.get('students', []))
page_token = response.get('nextPageToken', None)
if not page_token:
break
return students
def createAssignment(self,course_id,**kwargs):
service = build('classroom', 'v1', http=self.http_auth)
date, time = kwargs["dueDate"].split(" ")
yy,mm,dd = date.split("-")
h,m,s = time.split(":")
courseWork = {
'title': kwargs["title"],
'description': kwargs["desc"],
'materials': [
{'link': { 'url': kwargs["link"] } },
],
'dueDate': {
"month": mm,
"year": yy,
"day": dd
},
'dueTime':{
"hours": h,
"minutes": m,
"seconds": s
},
'workType': 'ASSIGNMENT',
'state': 'PUBLISHED',
}
courseWork = service.courses().courseWork().create(courseId=course_id, body=courseWork).execute()
return courseWork
def submitAssignment(self,**kwargs):
service = build('classroom', 'v1', http=self.http_auth)
course_id = kwargs["courseId"]
courseWorkId = kwargs["courseWorkId"]
score = kwargs["score"]
studentSubmission = {
'assignedGrade': score,
'draftGrade': score,
'assignmentSubmission': {
'attachments': [
{
'link': {
"url": "demo.com",
"title": "Assignment1",
"thumbnailUrl": "demo.com",
}
}
],
},
'state': 'TURNED_IN',
}
gCredentials = json.loads(self.credentials.to_json())
userGId = gCredentials["id_token"]["sub"]
studentSubmissionsData = service.courses().courseWork().studentSubmissions().list(
courseId=course_id,
courseWorkId=courseWorkId,
userId=userGId).execute()
studentSubmissionId = studentSubmissionsData["studentSubmissions"][0]["id"]
courseWorkRes = service.courses().courseWork().studentSubmissions().patch(
courseId=course_id,
courseWorkId=courseWorkId,
id=studentSubmissionId,
updateMask='assignedGrade,draftGrade',
body=studentSubmission).execute()
return courseWorkRes
Method Calling
g = Google()
kwargs = {"courseId":courseId,"courseWorkId":courseWorkId,"score":80}
courseworkResponse = g.submitAssignment(**kwargs)
Error:
https://classroom.googleapis.com/v1/courses/{courses_id}/courseWork/{courseWork_id}/studentSubmissions/{studentSubmissions_id}?alt=json&updateMask=assignedGrade%2CdraftGrade
returned "The caller does not have permission">
Student's submission contains following fields assignedGrade, draftGrade, attachments (Link resource) and state.
The call is being made from an authenticated student account. The Developer Console project has the Google Classroom API enabled, and other calls to the Google Classroom API are working fine, such as courses.courseWork.create and courses.courseWork.studentSubmissions.list. Also I am making request from the same Developer Console project from course work item is associated/created.
The same error 403 error with different message is returned when I am trying from Google API explorer.
{
"error": {
"code": 403,
"message": "#ProjectPermissionDenied The Developer Console project is not permitted to make this request.",
"status": "PERMISSION_DENIED"
}
}
Any help would be appreciated, thank you
That error message basically means that you don't have permission to do what it is you are trying to do. The permissions are related to what scopes you have authenticated the user with. here is a full list of scopes
Method: courses.courseWork.studentSubmis.patch requires the following scopes.
Authorization
Requires one of the following OAuth scopes:
https://www.googleapis.com/auth/classroom.coursework.students
https://www.googleapis.com/auth/classroom.coursework.me
Use List and Get before patch
Use list and get before patch to be sure that you have the corect ids.
If you have the user preform a list first then find the one you are after then preform a get you can make changes to the object in the get then run your update on that. Doing it this way ensures that all the ids you are passing are correct and that the user does in fact have access to what they are trying to update.
Related
I created a simple bot to find the weather in a city.
I have a lambda that calls an API and gets the weather of a city. I tested the lambda and it works fine. I configured the lex bot to call the lambda. I am going to post the lambda code, bot screeen shots and testing put that I used to test my lambda
from urllib.request import Request, urlopen
import json
from pprint import pprint
import urllib3
def lambda_handler(event, context):
pprint('received request: ' + str(event))
city = event['currentIntent']['slots']['city']
#print(city)
citytemperature = get_city_temperature(city)
#print('city temp is ' + str(citytemperature))
response = {
"dialogAction": {
"type": "Close",
"fulfillmentState": "Fulfilled",
"message": {
"contentType": "SSML",
"content": format(citytemperature)
},
}
}
#print('result = ' + str(response))
return response
def get_city_temperature(city):
print('weather of the city , city = ' + str(city))
api_key = 'myapikeyjkjkjkj'
base_url = 'http://api.openweathermap.org/data/2.5/weather?'
finalurl = base_url + 'appid=' + api_key + '&q=' + city
#print(finalurl)
httprequest = urllib3.PoolManager()
response = httprequest.request('GET',finalurl)
weather_status = json.loads(response.data.decode('utf-8'))
return weather_status["main"]["temp"]
This is the test data I used to test the lambda and it works fine when I run the lambda by itself. When I call this lambda from the lex bot, I don't see it being invoked at all. In the cloud watch logs I see this error:
[ERROR] KeyError: 'currentIntent'
Traceback (most recent call last):
File "/var/task/lambda_function.py", line 8, in lambda_handler
city = event['currentIntent']['slots']['city']
[ERROR] KeyError: 'currentIntent' Traceback (most recent call last): File
"/var/task/lambda_function.py", line 8, in lambda_handler city =
event['currentIntent']['slots']['city']
This is the documentation that says what should be sent as an input into Lambda and what should the response from lambda to the bot should look like.
https://docs.aws.amazon.com/lex/latest/dg/lambda-input-response-format.html
This is the test data to test the bot in lambda. Lambda works fine. If you change city name to something, it finds the temperature of the city
{
"messageVersion": "1.0",
"invocationSource": "FulfillmentCodeHook",
"userId": "AROAVNZMIA3MR7EYI4M7O",
"sessionAttributes": {},
"requestAttributes": "None",
"bot": {
"name": "Weather",
"alias": "$LATEST",
"version": "$LATEST"
},
"outputDialogMode": "Text",
"currentIntent": {
"name": "FindWeather",
"slots": {
"city": "pomona"
},
"slotDetails": {
"city": {
"resolutions": [],
"originalValue": "pomona"
}
},
"confirmationStatus": "None"
},
"inputTranscript": "whats the temperature in pomona?"
}
[![enter image description here][1]][1]
I believe your issue is a simple one. You have coded your Lambda function against Version 1 of Lex lambda schema.
The input and output message formats for Lex V2 are considerably different. You'll find the sample request and response formats here: https://docs.aws.amazon.com/lexv2/latest/dg/lambda.html#lambda-input-format
In particular, you will find the currentIntent equivalent within sessionState in the V2 input payload. So, your code to retrieve the users value for city should look like this:
city = event['sessionState']['intent']['slots']['city']['interpretedValue']
Also, you will need to change your response message to ensure it matches with what Lex V2 expects. It should look something like this:
event['sessionState']['intent']['state'] = 'Fulfilled'
return {
'sessionState': {
'sessionAttributes': session_attributes,
'dialogAction': {
'type': 'Close'
},
'intent': event['sessionState']['intent']
},
'messages': [message],
'sessionId': event['sessionId'],
'requestAttributes': event['requestAttributes'] if 'requestAttributes' in event else None
}
Here's the guide for the output format for V2: https://docs.aws.amazon.com/lexv2/latest/dg/lambda.html#lambda-response-format
In my .Net Core web API protected by IdentityServer4, I need to decide what identity provider (Google, Windows, or local, for instance) authenticated the user. So far, I am not sure how to do that.
If I search for idp claim from access_token in a controller, as shown below, I can see the claim value correctly
var accessToken = await HttpContext.GetTokenAsync("access_token");
var token = new JwtSecurityTokenHandler().ReadJwtToken(accessToken);
var claim = token.Claims.First(c => c.Type == "idp").Value;
But if I try to find it using AuthorizationHandlerContext in a non-controller class in API as following, as shown in code below, it is not there
var identity = context.User.Identity as ClaimsIdentity;
if (identity != null)
{
IEnumerable<Claim> claims = identity.Claims;
// var v = identity.FindFirst("idp").Value;
}
So looks like that idp is indeed in the token, it just not accessible from the non-controller class where it is needed. How do I get idp from non-controller class in API?
UPDATE - 1
Here is my ConfigureService in my API
public void ConfigureServices(IServiceCollection services)
{
IdentityModelEventSource.ShowPII = true; // test only
services.AddControllers();
services.AddControllers()
.AddNewtonsoftJson(
options => options.SerializerSettings.ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Ignore
);
});
services.Configure<QLHostOptions>(Configuration.GetSection(QLHostOptions.Host));
services.AddAuthentication("Bearer").AddJwtBearer("Bearer", options =>
{
options.Authority = Configuration.GetSection(QLHostOptions.Host).Get<QLHostOptions>().IdentityGateway;
options.SaveToken = true;
// test only
options.RequireHttpsMetadata = false;
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
}).AddOpenIdConnect(options =>
{
options.ClaimActions.Remove("aud");
});
services.AddTransient<IAuthorizationPolicyProvider, QLPolicyProvider>();
services.AddTransient<IAuthorizationHandler, QLPermissionHandler>();
services.AddTransient<gRPCServiceHelper>();
}
UPDATE-2
Changed ...Remove("idp") to inside AddJwtBearer, as Tory suggested, but it doesn't take it (see screenshot below):
and here is the access token from API
"eyJhbGciOiJSUzI1NiIsImtpZCI6IjBFM0Y2MkRGMTdFQUExQURFRTc1NDQzQzQ0M0YxRkU2IiwidHlwIjoiYXQrand0In0.eyJuYmYiOjE2NDAwMzExMDcsImV4cCI6MTY0MDAzNDcwNywiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NjAwNSIsImF1ZCI6Imh0dHBzOi8vbG9jYWxob3N0OjYwMDUvcmVzb3VyY2VzIiwiY2xpZW50X2lkIjoibXZjIiwic3ViIjoiZGM0YWI1OGMtNGVjMC00ZTAyLWIxM2YtYzEyYzk1MzJlNzcyIiwiYXV0aF90aW1lIjoxNjQwMDMxMTA2LCJpZHAiOiJHb29nbGUiLCJBc3BOZXQuSWRlbnRpdHkuU2VjdXJpdHlTdGFtcCI6IjJEQ0hXNVRER1E3NDNSUEpOWE43SVJIWlRIVllIUTRJIiwibmFtZSI6IkxpZmVuZyBYdSIsImVtYWlsIjoibGlmZW5neHUyNkBnbWFpbC5jb20iLCJyb2xlIjoiUUxBZG1pbiIsInByZWZlcnJlZF91c2VybmFtZSI6IjM1ZGJkMmY2LTlmNDUtNDJhYy04M2EzLTgzZmUyMTFjNTNiNSIsIklzRW5hYmxlZCI6IlRydWUiLCJRaWNMaW5rVUlEIjoiIiwianRpIjoiQzE3Qjc2QzQ0NjA4MzkxMDBENEExMEM4Q0YwQzA1NDEiLCJzaWQiOiIzMEY5NTA5NzQ3OUUxMzAyMUVBQTdDOTAzNzg4MDcxNiIsImlhdCI6MTY0MDAzMTEwNywic2NvcGUiOlsib3BlbmlkIiwicHJvZmlsZSIsImVtYWlsIiwiUWljTGlua0NJRCIsIlFpY0xpbmtVSUQiLCJyb2xlcyIsIklzRW5hYmxlZCIsIkxpZmVuZ0FQSSIsIlFpY0xpbmtBUEkiLCJvZmZsaW5lX2FjY2VzcyJdLCJhbXIiOlsiZXh0ZXJuYWwiXX0.boZCqYImWfkE48X5UgFOAAz9bR6CH2cwAYHGd4Ykg0vDH9qnYdje5Zmqov4HpINsu_rt16zxAX_JCEn0hvdznXK2NQyZSBGsjF0tcMgtOY0__kAfhpOT-fORakiIjeMWIKG7tPEHCxSib0wNuMNw6i3o1giAnPt0ch2DH0fBtaEYkq4MRKMCteFuqbX0cogXIuMewNywMvrHv4_MixhMy3L8_xIwFvTZ67jhUn4Fd5X58-jc-RPNudcP95XIjzHm9OzWfgegV1IAKjsv98XEYX1pUxm-nrOMgYWxEJSyxEpp0L_9RzKTr_LZ-ep-x5QRvVewgiozJV3mse0pHgTjbw"
By default many of the more internal claims in a token are removed from the User ClaimsPrinicpal claims.
If you want to get a specific claim into your user, you can use in the client:
}).AddOpenIDConnect(options =>
{
//Will result in that the aud claim is not removed.
options.ClaimActions.Remove("idp");
...
secondly, some of the claims are renamed and if you want to disable that renaming, you can add:
// By default, Microsoft has some legacy claim mapping that converts
// standard JWT claims into proprietary ones. This removes those mappings.
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
JwtSecurityTokenHandler.DefaultOutboundClaimTypeMap.Clear();
For the API you should not need to do anything special to get the idp claim. I just ran a test with this setup in .NET 5:
public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddMyJwtBearer(opt =>
{
opt.IncludeErrorDetails = true;
opt.MapInboundClaims = false;
opt.TokenValidationParameters.RoleClaimType = "role";
opt.TokenValidationParameters.NameClaimType = "name";
opt.Audience = "paymentapi";
opt.Authority = "https://localhost:6001";
});
services.AddControllers();
}
I did give it a test on .NET 5 and if I have this access token:
{
"nbf": 1640033816,
"exp": 1640037416,
"iss": "https://localhost:6001",
"aud": "paymentapi",
"client_id": "clientcredentialclient",
"managment": "yes",
"email": "tore#tn-data.se",
"name": "tore nestenius",
"idp": "Google",
"role": [
"admin",
"developer",
"support"
],
"website": "https://www.tn-data.se",
"jti": "5DC46A29372031F0AA6F7B62B5FDCCD6",
"iat": 1640033816,
"scope": [
"payment"
]
}
Then my user in my API controller contains the idp claim:
I have created Lex Chatbot and developed a website and integrated this Chatbot. Its working fine.But response cards in the form of buttons are not showing up.I got to know that I have to invoke it from lambda function.So I included the response card code .It works ,but after displaying the buttons it goes back and asks the first slot value again.I dont know where I am wrong
Here is the expected conversation.
User:Hi
Lex:Please provide me your eid
User:e123456
Lex:Choose one of the impact below:
1.low 2.high 3.medium (in form of buttons)
User clicks on low
Lex:Thanks,your ticket has been raised(expected)
What happens:
User:Hi
Lex:Please provide me your eid
User:e123456
Lex:Choose one of the impact below:
1.low 2.high 3.medium
User clicks on low
Lex:Please provide me your eid(goes back and asks the first slot value)
Here is my code:
import json
import logging
import re
import http.client
import mimetypes
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)
def elicit_slot_response(output_session_attributes,intent_name,slot_to_elicit,message):
responses= {
'dialogAction': {
'type': 'ElicitSlot',
'slottoElicit':'slot_to_elicit',
'message': {
'contentType': 'PlainText',
'content': message
},
'responseCard': {
'version': '0',
'contentType': 'application/vnd.amazonaws.card.generic',
'genericAttachments': [
{
'title': 'title1',
'subTitle': 'subtitle',
"buttons":[
{
"text":"button 1",
"value":"value 1"
},
{
"text":"button 2",
"value":"value 2"
},
{
"text":"button 3",
"value":"value 3"
}
]
}
]
}
}
}
return responses
def close():
val= {
"dialogAction":
{
"fulfillmentState":"Fulfilled",
"type":"Close",
"message":
{
"contentType":"PlainText",
"content":"Hey your ticket has been raised"
}
}
}
print(val)
return val
def lambda_handler(event, context):
val = ""
slots = event['currentIntent']['slots']
empidemployee= event['currentIntent']["slots"]["empidemployee"]
latestdesc= event['currentIntent']["slots"]["latestdesc"]
latestimpact= event['currentIntent']["slots"]["latestimpact"]
output_session_attributes = event['sessionAttributes'] if event['sessionAttributes'] is not None else {}
elicit_slot_response(output_session_attributes,'latestdetails','latestimpact',"impact")
val=close()
return val
The conversation flow restarts because in the ElicitSlot response from the Lambda function containing the response cards, you are not returning the slots parameter which would contain the slot values already taken as an input from the user.
So, include the slots parameter in the response the value for which could be event['currentIntent']['slots'].
Followed the best practive create nested objects with serializers, however I still receive empty nested validate_data.
Serializers:
class WriteOrganisationSiteSerializer(serializers.ModelSerializer):
"""Organisation Site serializer class for post methods."""
site = WriteAPSiteSerializer()
class Meta:
model = models.OrganisationSite
fields = ("organisation", "site")
def create(self, validated_data):
from fadat.sites.models import APSite
site_data = validated_data.pop("site")
activeplaces_site_id = site_data.pop("activeplaces_site_id")
site, created = APSite.objects.get_or_create(
activeplaces_site_id=activeplaces_site_id, defaults=site_data
)
organisation_site = models.OrganisationSite.objects.create(
site=site, **validated_data
)
return organisation_site
class WriteAPSiteSerializer(serializers.Serializer):
"""Active Places Site serializer class for post methods."""
class Meta:
model = models.APSite
fields = (
"activeplaces_site_id",
"site_name",
"dependent_thoroughfare",
"thoroughfare_name",
"double_dependent_locality",
"dependent_locality",
"post_town",
"postcode",
"easting",
"northing",
"longitude",
"latitude",
)
The view
class OrganisationSitesView(APIView):
"""Organisation Sites API view."""
def post(self, request, **kwargs):
user = request.user
ser = serializers.WriteOrganisationSiteSerializer(data=request.data)
ser.is_valid(raise_exception=True)
obj = ser.save()
ser = serializers.ReadOrganisationSiteSerializer(obj)
return Response(ser.data, status=201)
Running the following test (or via browser a ajax request, same result)
def test_add_organisation_site(self):
user = User.objects.create(email="newbie#dat.example.com")
organisation_type = OrganisationType.objects.create(name="Club")
organisation = Organisation.active_objects.create(
name="Club", organisation_type=organisation_type
)
site = {
"activeplaces_site_id": 1200341,
"site_name": "CITY OF LONDON SCHOOL",
"dependent_thoroughfare": "",
"thoroughfare_name": "QUEEN VICTORIA STREET",
"double_dependent_locality": "",
"dependent_locality": "",
"post_town": "LONDON",
"postcode": "EC4V 3AL",
"easting": 531990,
"northing": 180834,
"longitude": -0.099387,
"latitude": 51.511025,
}
body = {
"organisation": organisation.id,
"site": site,
}
self.authenticate(user)
url = reverse("api:inspections:organisation-sites")
res = self.client.post(url, json.dumps(body), content_type="application/json; charset=utf-8")
self.assertEqual(res.status_code, 201)
Receiving the following headers in my views
{'organisation': 1, 'site': {'activeplaces_site_id': 1200341, 'site_name': 'CITY OF LONDON SCHOOL', 'dependent_thoroughfare': '', 'thoroughfare_name': 'QUEEN VICTORIA STREET', 'double_dependent_locality': '', 'dependent_locality': '', 'post_town': 'LONDON', 'postcode': 'EC4V 3AL', 'easting': 531990, 'northing': 180834, 'longitude': -0.099387, 'latitude': 51.511025}}
In the view the request.data show me
{'Cookie': '', 'Content-Length': '368', 'Content-Type': 'application/json; charset=utf-8', 'Authorization': 'Token 0081d8a36d90f1d922a2a7df494afe127a220495'}
Still the serialized doesn't validate nested fields and returns
{'organisation': <Organisation: Club (Club)>, 'site': OrderedDict()}
Right, here's what I see that needs fixing: the conventional way to design a post()method is as follows:
def post(self, request, **kwargs):
serializer = serializers.WriteOrganisationSiteSerializer(data=request.data)
serializer.is_valid(raise_exception=True)
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED, headers=headers)
Although your case is in many ways equivalent, it's obviously not 100% correct. Perhaps it has to do with the fact that you're using one serializer to create the new instance, and another to show the result, when that's not needed: you can set up which fields are read_only, which ones are write_only, and those that are both read and write, as the documentation explains. I suggest you follow the schema I laid out above to guide you, and define in WriteOrganisationSiteSerializer how to show the data to the end user. Do let me know if you have any problems.
I'm trying to add Django-notifications to my drf project. I get response when hitting the endpoint:
[
{
"recipient": {
"id": 274,
"username": "harry",
"first_name": "Harry",
"last_name": "Moreno"
},
"unread": true,
"target": null,
"verb": "invite approved"
}
]
serializers.py
class GenericNotificationRelatedField(serializers.RelatedField):
User = get_user_model()
def to_representation(self, value):
if isinstance(value, Invite):
serializer = InviteSerializer(value)
if isinstance(value, User):
serializer = UserSerializer(value)
return serializer.data
class NotificationSerializer(serializers.Serializer):
recipient = UserSerializer(read_only=True)
unread = serializers.BooleanField(read_only=True)
target = GenericNotificationRelatedField(read_only=True)
How do I make the target non-null?
Turns out the target is null because that is how I created the notification in the model
notify.send(user, recipient=user, verb='you reached level 10')
if I wanted a non-null target I should specify one like
notify.send(user, recipient=user, target=user, verb='you reached level 10')
Note: there is no django view that generates the json in the question.
In our urls.py we wire up the route to the notification view from the app.
path(
"alerts/",
views.NotificationViewSet.as_view({"get": "list"}),
name="notifications",
),
see the installation instructions https://github.com/django-notifications/django-notifications#installation