I'm trying to set up a WAMP server that can handle session data of individual clients. However this seems more troublesome than initially thought.
Crossbar config:
{
"workers": [
{
"type": "router",
"realms": [
{
"name": "default",
"roles": [
{
"name": "anonymous",
"permissions": [
{
"uri": "*",
"call": true,
"register": true
}
]
}
]
}
],
"transports": [
{
"type": "websocket",
"endpoint": {
"type": "tcp",
"port": 8080
}
}
],
"components": [
{
"type": "class",
"classname": "server.Server",
"realm": "default",
"role": "anonymous"
}
]
}
]
}
server.py:
The server registers two RPCs, one for appending data and one for returning a string of the data. The data is stored as self.data, but this is storing data for all sessions, not on a per client, per session basis. Once the session dies, the server should clean up the session data. Simply cleaning the list is no solution as simultaneous clients can access each others data.
The id of the client is available in the append RPC (if the client discloses itself), however this seems fairly useless at this point.
from autobahn.twisted import wamp
from autobahn.wamp import types
class Server(wamp.ApplicationSession):
def __init__(self, *args, **kwargs):
wamp.ApplicationSession.__init__(self, *args, **kwargs)
self.data = []
def onJoin(self, details):
def append(data, details):
client_id = details.caller
self.data.append(data)
def get():
return ''.join(self.data)
options = types.RegisterOptions(details_arg='details')
self.register(append, 'append', options=options)
self.register(get, 'get')
client.py:
The client connects to the server, waits for the connection to open and executes RPCs. The client first appends 'a' and 'b' to the server's data, then the data is get and printed. The result should be ab as data should be stored per client, per session. Once the session dies, the data should be cleaned up.
import asyncio
from autobahn.asyncio import wamp
from autobahn.asyncio import websocket
from autobahn.wamp import types
class Client(wamp.ApplicationSession):
def onOpen(self, protocol):
protocol.session = self
wamp.ApplicationSession.onOpen(self, protocol)
if __name__ == '__main__':
session_factory = wamp.ApplicationSessionFactory()
session_factory.session = Client
transport_factory = websocket.WampWebSocketClientFactory(session_factory)
loop = asyncio.get_event_loop()
transport, protocol = loop.run_until_complete(
asyncio.async(loop.create_connection(
transport_factory, 'localhost', '8080',)))
connected = asyncio.Event()
#asyncio.coroutine
def poll():
session = getattr(protocol, 'session', None)
if not session:
yield from asyncio.sleep(1)
asyncio.ensure_future(poll())
else:
connected.set()
# Wait for session to open.
asyncio.ensure_future(poll())
loop.run_until_complete(connected.wait())
# Client is connected, call RPCs.
options = types.CallOptions(disclose_me=True)
protocol.session.call('append', 'a', options=options)
protocol.session.call('append', 'b', options=options)
f = protocol.session.call('get', options=options)
# Get stored data and print it.
print(loop.run_until_complete(f))
Hope somebody can tell me how I can store data per client, per session in the server's memory.
I managed to create a hack for this. It doesn't seem like the perfect solution, but it works for now.
from autobahn.twisted import wamp
from autobahn.wamp import types
from twisted.internet.defer import inlineCallbacks
class Server(wamp.ApplicationSession):
def __init__(self, *args, **kwargs):
wamp.ApplicationSession.__init__(self, *args, **kwargs)
self.sessions = {}
def onJoin(self, details):
def on_client_join(details):
client_id = details['session']
self.sessions[client_id] = {}
def on_client_leave(client_id):
self.sessions.pop(client_id)
self.subscribe(on_client_join, 'wamp.session.on_join')
self.subscribe(on_client_leave, 'wamp.session.on_leave')
def get_session(details):
return self.sessions[details.caller]
#inlineCallbacks
def append(data, details):
session = yield self.call('get_session', details)
d = session.setdefault('data', [])
d.append(data)
#inlineCallbacks
def get(details):
session = yield self.call('get_session', details)
return ''.join(session['data'])
reg_options = types.RegisterOptions(details_arg='details')
self.register(get_session, 'get_session')
self.register(append, 'append', options=reg_options)
self.register(get, 'get', options=reg_options)
As session gets created when a client connects (on_client_join), and the session gets destroyed when a client disconnects (on_client_leave).
The server also needs permission to subscribe to the meta events. config.json:
...
"permissions": [
{
"uri": "*",
"call": true,
"register": true,
"subscribe": true,
}
]
....
Related
There are two requests, the first is reading the task by uuid, the second is outputting 3 random tasks from the same user - "recommendations"
The task that is open
{
"id": 4,
"userInfo": 1,
"title": "Comparing numbers",
"uuid": "5a722487"
}
Recommendations for it
Tell me, how to exclude the current task from the second query
[
{
"id": 16,
"userInfo": 1,
"title": "The opposite number",
"uuid": "1e6a7182"
},
{
"id": 19,
"userInfo": 1,
"title": "Number of vowels",
"uuid": "be1320cc"
},
{
**"id": 4, <- exclude this post
"userInfo": 1,
"title": "Comparing numbers",
"uuid": "5a722487"**
}
]
views.py
class PostUuid(generics.ListAPIView):
"""Reading a record by uuid"""
queryset = Task.objects.all()
serializer_class = TaskCreateSerializer
lookup_field = 'uuid'
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
Task.objects.filter(pk=instance.id)
serializer = self.get_serializer(instance)
return Response(serializer.data)
def get(self, request, *args, **kwargs):
return self.retrieve(request, *args, **kwargs)
class RecommendationTaskView(generics.ListAPIView):
"""Getting a recommendation"""
serializer_class = TaskCreateSerializer
def get_queryset(self):
items = list(Task.objects.filter(
userInfo_id=self.kwargs.get('pk')).select_related('userInfo'))
random_items = random.sample(items, 3)
return random_items
Restful APIs should be stateless. Statelessness means that every HTTP request happens in complete isolation. When the client makes an HTTP request, it includes all information necessary for the server to fulfill the request.
The server never relies on information from previous requests from the client. If any such information is important then the client will send that as part of the current request.
You should send the task id which you want to exclude on the other apis. In this way, you have that id and you can exclude that on the query set.
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
I want to add status field to error response, so instead of this:
{
"errors": [
{
"message": "Authentication credentials were not provided",
"locations": [
{
"line": 2,
"column": 3
}
]
}
],
"data": {
"viewer": null
}
}
It should be like this:
{
"errors": [
{
"status": 401, # or 400 or 403 or whatever error status suits
"message": "Authentication credentials were not provided",
"locations": [
{
"line": 2,
"column": 3
}
]
}
],
"data": {
"viewer": null
}
}
I found out that I only can change message by raising Exception inside resolver: raise Error('custom error message'), but how to add field?
Code example:
class Query(UsersQuery, graphene.ObjectType):
me = graphene.Field(SelfUserNode)
def resolve_me(self, info: ResolveInfo):
user = info.context.user
if not user.is_authenticated:
# but status attr doesn't exist...
raise GraphQLError('Authentication credentials were not provided', status=401)
return user
Update the default GraphQLView with the following:
from graphene_django.views import GraphQLView as BaseGraphQLView
class GraphQLView(BaseGraphQLView):
#staticmethod
def format_error(error):
formatted_error = super(GraphQLView, GraphQLView).format_error(error)
try:
formatted_error['context'] = error.original_error.context
except AttributeError:
pass
return formatted_error
urlpatterns = [
path('api', GraphQLView.as_view()),
]
This will look for the context attribute in any exceptions raised. If it exists, it'll populate the error with this data.
Now you can create exceptions for different use cases that populate the context attribute. In this case you want to add the status code to errors, here's an example of how you'd do that:
class APIException(Exception):
def __init__(self, message, status=None):
self.context = {}
if status:
self.context['status'] = status
super().__init__(message)
You'd use it like this:
raise APIException('Something went wrong', status=400)
I didn't found a way to solve your problem int the way that you propose, otherwise i extend the LoginRequiredMixin class like this:
class LoginRequiredMixin:
def dispatch(self, info, *args, **kwargs):
if not info.user.is_authenticated:
e = HttpError(HttpResponse(status=401, content_type='application/json'), 'Please log in first')
response = e.response
response.content = self.json_encode(info, [{'errors': [self.format_error(e)]}])
return response
return super().dispatch(info, *args, **kwargs)
class PrivateGraphQLView(LoginRequiredMixin, GraphQLView):
schema=schema
and in your url:
from django.views.decorators.csrf import csrf_exempt
from educor.schema import PrivateGraphQLView
url(r'^graphql', csrf_exempt(PrivateGraphQLView.as_view(batch=True)))
you can't see the status with the graphiql but in your client you can get it in the headers or you could modify this line to add in the response response.content = self.json_encode(info, [{'errors': [self.format_error(e)]}])
. Hope it helps anyway i'll leave you another possible solution https://github.com/graphql-python/graphene-django/issues/252
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.
I want to let my meteor users login through a ruby app.
Where I am
I have two websites, both on the same domain, both share the same MongoDB.
One is a METEOR-app with accounts-password (which uses bcrypt)
The other is a RUBY ON RAILS-app which uses devise (which also uses bcrypt) for authentication.
On both apps I can register and login, separately.
When I transfer (copy/paste) the encrypted_password from Meteor's "bcrypt" field to Ruby's "encrypted_password" and try to login I get rejected. It does not work vice versa. Then I recreated the kind of salting by the meteor app in my ruby app (SHA-256 plain-password-hashing before they got compared against).
(here is the meteor accounts-password source file (https://github.com/meteor/meteor/blob/oplog-backlog-on-1.0.3.1/packages/accounts-password/password_server.js ))
and this is my Ruby implementation:
class BCryptSHA256Hasher < Hasher
def initialize
#algorithm = :bcrypt_sha256
#cost = 10
#digest = OpenSSL::Digest::SHA256.new
end
def salt
BCrypt::Engine.generate_salt(#cost)
end
def get_password_string(password)
#digest.digest(password) unless #digest.nil?
end
def encode(password, salt)
password = get_password_string(password)
hash = BCrypt::Engine.hash_secret(password, salt)
return hash
end
def verify(password, encoded)
password_digest = get_password_string(password)
hash = BCrypt::Engine.hash_secret(password_digest, encoded)
# password = "asdfasdf"
# encoded = "$2a$10$FqvtI7zNgmdWJJG1n9JwZewVYrzEn38JIxEGwmMviMsZsrCmYHqWm"
# hash = "$2a$10$FqvtI7zNgmdWJJG1n9JwZe22XU1hRDSNtHIrnYve9FbmjjqJCLhZi"
# constant_time_comparison:
constant_time_compare(encoded, hash)
end
def constant_time_compare(a, b)
check = a.bytesize ^ b.bytesize
a.bytes.zip(b.bytes) { |x, y| check |= x ^ y }
check == 0
end
end
Here is a valid User-document, which will be used by both servers:
{
"_id": "g4BPfpavJGGTNgJcE",
"authentication_token": "iZqmCsYS1Y9Xxh6t22-X",
"confirmed_at": new Date(1457963598783),
"createdAt": new Date(1457963456581),
"current_sign_in_at": new Date(1457966356123),
"current_sign_in_ip": "127.0.0.1",
"email": "demo#demo.com",
"emails": [
{
"address": "demo#demo.com",
"verified": true
}
],
"encrypted_password": "$2a$10$7/PJw51HgXfzYJWpaBHGj.QoRCTl0E29X0ZYTZPQhLRo69DGi8Xou",
"failed_attempts": 0,
"last_sign_in_at": new Date(1457966356123),
"last_sign_in_ip": "127.0.0.1",
"profile": {
"_id": ObjectId("56e6c1e7a54d7595e099da27"),
"firstName": "asdf",
"lastName": "asdf"
},
"reset_password_sent_at": null,
"reset_password_token": null,
"services": {
"_id": ObjectId("56e6c1e7a54d7595e099da28"),
"password": {
"bcrypt": "$2a$10$7/PJw51HgXfzYJWpaBHGj.QoRCTl0E29X0ZYTZPQhLRo69DGi8Xou"
},
"resume": {
"loginTokens": [
]
}
},
"sign_in_count": 1,
"updated_at": new Date(1457966356127),
"username": "mediatainment"
}
I think #maxpleaner's comment is the best way to handle authentication. But if really need to authenticate users separately, then just monkey patch devise.
config/initializers/devise_meteor_adapter.rb
module DeviseMeteorAdapter
def digest(klass, password)
klass.pepper = nil
password = ::Digest::SHA256.hexdigest(password)
super
end
def compare(klass, hashed_password, password)
klass.pepper = nil
password = ::Digest::SHA256.hexdigest(password)
super
end
end
Devise::Encryptor.singleton_class.prepend(DeviseMeteorAdapter)
WARNING: Not tested.