Skip to content
GitLab
Projects
Groups
Snippets
/
Help
Help
Support
Community forum
Keyboard shortcuts
?
Submit feedback
Sign in
Toggle navigation
Menu
Open sidebar
feudal
feudalBackend
Commits
d45e4e76
Commit
d45e4e76
authored
Jun 12, 2018
by
Lukas Burgey
Browse files
Implement backchannel from the client to the backend
parent
0cc4ef92
Changes
6
Hide whitespace changes
Inline
Side-by-side
django_backend/backend/auth/v1/models.py
View file @
d45e4e76
...
...
@@ -55,7 +55,7 @@ class OIDCConfig(db_models.Model):
args
=
{
'client_id'
:
self
.
client_id
,
'response_type'
:
'code'
,
'scope'
:
[
'openid'
,
'profile'
,
'email'
],
'scope'
:
[
'openid'
,
'profile'
,
'email'
,
'credentials'
],
'redirect_uri'
:
self
.
redirect_uri
,
'state'
:
state
,
}
...
...
@@ -79,33 +79,45 @@ def default_idp():
class
OIDCTokenAuthBackend
(
object
):
def
get_userinfo
(
self
,
request
,
access_token
,
token_type
=
'Bearer'
):
idp_id
=
utils
.
get_session
(
request
,
'idp_id'
,
None
)
idp
=
OIDCConfig
.
objects
.
get
(
id
=
idp_id
)
req
=
Request
(
idp
.
oidc_client
.
provider_info
[
'userinfo_endpoint'
]
)
auth
=
(
token_type
+
' '
+
access_token
)
req
.
add_header
(
'Authorization'
,
auth
)
def
get_userinfo
(
self
,
request
,
oidc_client
,
access_token
=
''
,
state
=
None
):
user_info
=
None
if
access_token
is
not
None
:
req
=
Request
(
oidc_client
.
provider_info
[
'userinfo_endpoint'
]
+
'?scope=openid&scope=profile'
,
)
auth
=
(
'Bearer '
+
access_token
)
req
.
add_header
(
'Authorization'
,
auth
)
userinfo_bytes
=
urlopen
(
req
).
read
()
user_info
=
json
.
loads
(
userinfo_bytes
.
decode
(
'UTF-8'
))
else
:
LOGGER
.
error
(
"Invalid parameters for get_userinfo"
)
userinfo_bytes
=
urlopen
(
req
).
read
()
return
json
.
loads
(
userinfo_bytes
.
decode
(
'UTF-8'
))
LOGGER
.
debug
(
"Got user info:
\n
%s
\n
"
,
user_info
)
return
user_info
def
authenticate
(
self
,
request
,
token
=
None
):
if
token
is
None
:
return
None
# get the user info from the idp
userinfo
=
self
.
get_userinfo
(
request
,
token
)
idp_id
=
utils
.
get_session
(
request
,
'idp_id'
,
None
)
idp
=
OIDCConfig
.
objects
.
get
(
id
=
idp_id
)
oidc_client
=
OIDCConfig
.
objects
.
get
(
id
=
idp_id
)
# get the user info from the idp
userinfo
=
self
.
get_userinfo
(
request
,
oidc_client
,
access_token
=
token
,
)
try
:
return
models
.
User
.
get_user
(
userinfo
,
id
p
,
o
id
c_client
,
)
except
Exception
as
exception
:
LOGGER
.
error
(
'OIDCTokenAuthBackend: error constructing user: %s'
,
exception
)
return
None
...
...
django_backend/backend/auth/v1/views.py
View file @
d45e4e76
...
...
@@ -64,9 +64,10 @@ class AuthCallback(View):
idp_id
=
utils
.
get_session
(
request
,
'idp_id'
,
default_idp
().
id
)
oidc_config
=
OIDCConfig
.
objects
.
get
(
id
=
idp_id
)
oidc_client
=
oidc_config
.
oidc_client
LOGGER
.
debug
(
'AuthCallback: %s returned from IdP %s'
,
state
,
oidc_config
)
aresp
=
oidc_config
.
oidc_client
.
parse_response
(
aresp
=
oidc_client
.
parse_response
(
AuthorizationResponse
,
info
=
json
.
dumps
(
request
.
GET
),
)
...
...
@@ -79,17 +80,22 @@ class AuthCallback(View):
raise
AuthException
(
'AuthCallbackStates do not match'
)
ac_token_response
=
(
oidc_config
.
oidc_client
.
do_access_token_request
(
state
=
aresp
[
'
state
'
]
,
oidc_client
.
do_access_token_request
(
state
=
state
,
request_args
=
{
'code'
:
aresp
[
'code'
]
},
)
)
# does fail with 'invalid_token'
# user_info = OIDC_CLIENT.do_user_info_request(
# statearesp['state'])
# FIXME 'email_verified' in user info is no boolean
# but oic expects it to be
#user_info = oidc_client.do_user_info_request(
# method="GET",
# state=state,
#)
#LOGGER.debug("EXPERIMENT: %s", user_info)
# user_info = self.get_user_info(ac_token_response['access_token'])
# try:
# u = models.User.objects.get(sub=user_info['sub'])
...
...
django_backend/backend/clientapi/urls.py
View file @
d45e4e76
...
...
@@ -5,4 +5,5 @@ URLPATTERNS = [
url
(
r
'^deployments'
,
views
.
DeploymentsView
.
as_view
()),
url
(
r
'^config'
,
views
.
ConfigurationView
.
as_view
()),
url
(
r
'^ack/(?P<task_id>\d+)'
,
views
.
AckView
.
as_view
()),
url
(
r
'^response'
,
views
.
ResponseView
.
as_view
()),
]
django_backend/backend/clientapi/views.py
View file @
d45e4e76
...
...
@@ -26,7 +26,6 @@ class ConfigurationView(views.APIView):
authentication_classes
=
AUTHENTICATION_CLASSES
def
get
(
self
,
request
):
response
=
{
'services'
:
ServiceSerializer
(
request
.
user
.
site
.
services
.
all
(),
...
...
@@ -36,7 +35,6 @@ class ConfigurationView(views.APIView):
RabbitMQInstance
.
load
(),
).
data
,
}
LOGGER
.
debug
(
'Config: %s'
,
response
)
return
Response
(
response
)
...
...
@@ -48,8 +46,41 @@ class AckView(views.APIView):
for
item
in
request
.
user
.
site
.
task_items
.
all
():
if
item
.
task
.
id
==
int
(
task_id
):
item
.
task
.
item_finished
(
request
.
user
.
site
)
LOGGER
.
debug
(
'Got acknowledgement for task %s'
,
task_id
)
return
Response
({
'ok'
:
True
})
# this is no critical
LOGGER
.
info
(
'%s executed the obsolete task#%s'
,
request
.
user
,
task_id
)
return
Response
({
'ok'
:
True
})
class
ResponseView
(
views
.
APIView
):
authentication_classes
=
AUTHENTICATION_CLASSES
def
post
(
self
,
request
):
status
=
request
.
data
[
'output'
][
'status'
]
task_id
=
request
.
data
[
'id'
]
LOGGER
.
debug
(
'%s responded to task %s:
\n
%s'
,
request
.
user
,
task_id
,
request
.
data
)
# find the corresponding task for this item
task_item
=
None
for
item
in
request
.
user
.
site
.
task_items
.
all
():
if
item
.
task
.
id
==
int
(
task_id
):
task_item
=
item
if
task_item
is
not
None
:
if
status
==
'success'
:
task_item
.
task
.
item_finished
(
request
.
user
.
site
)
return
Response
({
'ok'
:
True
})
elif
status
==
'fail'
:
task_item
.
task
.
item_failed
(
request
.
user
.
site
)
return
Response
({
'ok'
:
True
})
elif
status
==
'reject'
:
task_item
.
task
.
item_rejected
(
request
.
user
.
site
)
return
Response
({
'ok'
:
True
})
LOGGER
.
info
(
'%s executed the obsolete task#%s'
,
request
.
user
,
task_id
)
return
Response
({
'ok'
:
False
})
django_backend/backend/frontend/serializers.py
View file @
d45e4e76
...
...
@@ -32,6 +32,7 @@ class DeploymentTaskItemSerializer(serializers.ModelSerializer):
'key'
,
'service'
,
'site'
,
'state'
,
]
...
...
django_backend/backend/models.py
View file @
d45e4e76
...
...
@@ -102,7 +102,7 @@ class RabbitMQInstance(SingletonModel):
def
_init_connection
(
self
):
global
RABBITMQ_CONNECTION
LOGGER
.
info
(
'Opening new BlockingConnection'
)
#
LOGGER.
debug
('Opening new BlockingConnection')
RABBITMQ_CONNECTION
=
pika
.
BlockingConnection
(
pika
.
ConnectionParameters
(
host
=
self
.
host
,
...
...
@@ -141,7 +141,7 @@ class RabbitMQInstance(SingletonModel):
def
_publish
(
self
,
exchange
,
routing_key
,
body
):
channel
=
self
.
_channel
self
.
_
channel
.
basic_publish
(
channel
.
basic_publish
(
exchange
=
exchange
,
routing_key
=
routing_key
,
body
=
body
,
...
...
@@ -246,17 +246,16 @@ class User(AbstractUser):
sub
=
userinfo
[
'sub'
]
if
'email'
not
in
userinfo
:
if
'name'
not
in
userinfo
:
raise
Exception
(
'Missing attributes in userinfo: email and name'
)
username
=
userinfo
[
'name'
]
username
=
sub
else
:
username
=
userinfo
[
'email'
]
email
=
userinfo
[
'email'
]
user
=
cls
(
user_type
=
'oidcuser'
,
username
=
username
,
sub
=
sub
,
email
=
email
,
idp
=
idp
,
userinfo
=
userinfo
,
)
...
...
@@ -715,6 +714,17 @@ class DeploymentTask(models.Model):
msg
,
)
# update the state of the remote webpage
def
send_state_update
(
self
):
from
.frontend.views
import
user_state_dict
content
=
{
'user_state'
:
user_state_dict
(
self
.
user
),
}
RabbitMQInstance
.
load
().
publish_to_webpage
(
self
.
user
,
content
,
)
# the client acked the receipt and execution of the task for his site
def
item_finished
(
self
,
site
):
...
...
@@ -726,14 +736,31 @@ class DeploymentTask(models.Model):
# finished sends its own message
self
.
_finished
()
else
:
from
.frontend.views
import
user_state_dict
content
=
{
'user_state'
:
user_state_dict
(
self
.
user
),
}
RabbitMQInstance
.
load
().
publish_to_webpage
(
self
.
user
,
content
,
)
self
.
send_state_update
()
# the client failed to execute the item
# the client can try again later
# we signal the user about the failure
def
item_failed
(
self
,
site
):
item
=
self
.
task_items
.
get
(
site
=
site
)
item
.
state
=
'failed'
item
.
save
()
self
.
send_state_update
()
# TODO implement
# the client failed to execute the item
# the client needs additional information from the user to try again
# we have to ask the user for data
def
item_rejected
(
self
,
site
):
item
=
self
.
task_items
.
get
(
site
=
site
)
item
.
state
=
'rejected'
item
.
save
()
self
.
send_state_update
()
# TODO implement
# maintenance after all task items are done
def
_finished
(
self
):
...
...
@@ -778,6 +805,18 @@ class DeploymentTaskItem(models.Model):
related_name
=
'deployment_task_items'
,
on_delete
=
models
.
CASCADE
,
)
STATE_CHOICES
=
(
(
'pending'
,
'Pending'
),
(
'done'
,
'Done'
),
(
'chanceled'
,
'Chanceled'
),
(
'failed'
,
'Failed'
),
(
'rejected'
,
'Rejected'
),
)
state
=
models
.
CharField
(
max_length
=
20
,
choices
=
STATE_CHOICES
,
default
=
'pending'
,
)
@
property
def
service
(
self
):
...
...
Write
Preview
Supports
Markdown
0%
Try again
or
attach a new file
.
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment