1 # -*- coding: utf-8 -*-
3 # Copyright 2018 Telefonica S.A.
4 # Copyright 2018 ALTRAN InnovaciĆ³n S.L.
6 # Licensed under the Apache License, Version 2.0 (the "License"); you may
7 # not use this file except in compliance with the License. You may obtain
8 # a copy of the License at
10 # http://www.apache.org/licenses/LICENSE-2.0
12 # Unless required by applicable law or agreed to in writing, software
13 # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
14 # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
15 # License for the specific language governing permissions and limitations
18 # For those usages not covered by the Apache License, Version 2.0 please
19 # contact: esousa@whitestack.com or glavado@whitestack.com
23 AuthconnInternal implements implements the connector for
24 OSM Internal Authentication Backend and leverages the RBAC model
28 "Pedro de la Cruz Ramos <pdelacruzramos@altran.com>, "
29 "Alfonso Tierno <alfonso.tiernosepulveda@telefoncia.com"
31 __date__
= "$06-jun-2019 11:16:08$"
36 from osm_nbi
.authconn
import (
39 AuthconnConflictException
,
40 ) # , AuthconnOperationException
41 from osm_common
.dbbase
import DbException
42 from osm_nbi
.base_topic
import BaseTopic
43 from osm_nbi
.utils
import cef_event
, cef_event_builder
44 from osm_nbi
.validation
import is_valid_uuid
45 from time
import time
, sleep
46 from http
import HTTPStatus
47 from uuid
import uuid4
48 from hashlib
import sha256
49 from copy
import deepcopy
50 from random
import choice
as random_choice
53 class AuthconnInternal(Authconn
):
54 token_time_window
= 2 # seconds
55 token_delay
= 1 # seconds to wait upon second request within time window
57 users_collection
= "users"
58 roles_collection
= "roles"
59 projects_collection
= "projects"
60 tokens_collection
= "tokens"
62 def __init__(self
, config
, db
, role_permissions
):
63 Authconn
.__init
__(self
, config
, db
, role_permissions
)
64 self
.logger
= logging
.getLogger("nbi.authenticator.internal")
68 # self.token_cache = token_cache
72 self
.cef_logger
= cef_event_builder(config
)
74 def validate_token(self
, token
):
76 Check if the token is valid.
78 :param token: token to validate
79 :return: dictionary with information associated with the token:
81 "project_id": project id
82 "project_name": project name
85 "roles": list with dict containing {name, id}
86 "expires": expiration date
87 If the token is not valid an exception is raised.
93 "Needed a token or Authorization HTTP header",
94 http_code
=HTTPStatus
.UNAUTHORIZED
,
99 # get from database if not in cache
101 token_info
= self
.db
.get_one(self
.tokens_collection
, {"_id": token
})
102 if token_info
["expires"] < now
:
104 "Expired Token or Authorization HTTP header",
105 http_code
=HTTPStatus
.UNAUTHORIZED
,
110 except DbException
as e
:
111 if e
.http_code
== HTTPStatus
.NOT_FOUND
:
113 "Invalid Token or Authorization HTTP header",
114 http_code
=HTTPStatus
.UNAUTHORIZED
,
118 except AuthException
:
121 self
.logger
.exception(
122 "Error during token validation using internal backend"
125 "Error during token validation using internal backend",
126 http_code
=HTTPStatus
.UNAUTHORIZED
,
129 def revoke_token(self
, token
):
133 :param token: token to be revoked
136 # self.token_cache.pop(token, None)
137 self
.db
.del_one(self
.tokens_collection
, {"_id": token
})
139 except DbException
as e
:
140 if e
.http_code
== HTTPStatus
.NOT_FOUND
:
142 "Token '{}' not found".format(token
), http_code
=HTTPStatus
.NOT_FOUND
146 exmsg
= "Error during token revocation using internal backend"
147 self
.logger
.exception(exmsg
)
148 raise AuthException(exmsg
, http_code
=HTTPStatus
.UNAUTHORIZED
)
150 def validate_user(self
, user
, password
):
152 Validate username and password via appropriate backend.
153 :param user: username of the user.
154 :param password: password to be validated.
156 user_rows
= self
.db
.get_list(
157 self
.users_collection
, {BaseTopic
.id_field("users", user
): user
}
162 user_rows
= self
.db
.get_list(
163 self
.users_collection
,
164 {BaseTopic
.id_field(self
.users_collection
, user
): user
},
167 user_content
= user_rows
[0]
168 # Updating user_status for every system_admin id role login
169 mapped_roles
= user_content
.get("project_role_mappings")
170 for role
in mapped_roles
:
171 role_id
= role
.get("role")
172 role_assigned
= self
.db
.get_one(
173 self
.roles_collection
,
174 {BaseTopic
.id_field(self
.roles_collection
, role_id
): role_id
},
177 if role_assigned
.get("permissions")["admin"]:
178 if role_assigned
.get("permissions")["default"]:
179 if self
.config
.get("user_management"):
181 users
= self
.db
.get_list(self
.users_collection
, filt
)
182 for user_info
in users
:
183 if not user_info
.get("username") == "admin":
184 if not user_info
.get("_admin").get(
185 "account_expire_time"
187 expire
= now
+ 86400 * self
.config
.get(
188 "account_expire_days"
191 self
.users_collection
,
192 {"_id": user_info
["_id"]},
193 {"_admin.account_expire_time": expire
},
196 if now
> user_info
.get("_admin").get(
197 "account_expire_time"
200 self
.users_collection
,
201 {"_id": user_info
["_id"]},
202 {"_admin.user_status": "expired"},
206 # To add "admin" user_status key while upgrading osm setup with feature enabled
207 if user_content
.get("username") == "admin":
208 if self
.config
.get("user_management"):
210 self
.users_collection
,
211 {"_id": user_content
["_id"]},
212 {"_admin.user_status": "always-active"},
215 if not user_content
.get("username") == "admin":
216 if self
.config
.get("user_management"):
217 if not user_content
.get("_admin").get("account_expire_time"):
218 account_expire_time
= now
+ 86400 * self
.config
.get(
219 "account_expire_days"
222 self
.users_collection
,
223 {"_id": user_content
["_id"]},
224 {"_admin.account_expire_time": account_expire_time
},
227 account_expire_time
= user_content
.get("_admin").get(
228 "account_expire_time"
231 if now
> account_expire_time
:
233 self
.users_collection
,
234 {"_id": user_content
["_id"]},
235 {"_admin.user_status": "expired"},
238 "Account expired", http_code
=HTTPStatus
.UNAUTHORIZED
241 if user_content
.get("_admin").get("user_status") == "locked":
243 "Failed to login as the account is locked due to MANY FAILED ATTEMPTS"
245 elif user_content
.get("_admin").get("user_status") == "expired":
247 "Failed to login as the account is expired"
250 salt
= user_content
["_admin"]["salt"]
251 shadow_password
= sha256(
252 password
.encode("utf-8") + salt
.encode("utf-8")
254 if shadow_password
!= user_content
["password"]:
256 if user_content
.get("_admin").get("retry_count") >= 0:
257 count
+= user_content
.get("_admin").get("retry_count")
259 self
.users_collection
,
260 {"_id": user_content
["_id"]},
261 {"_admin.retry_count": count
},
264 "Failed Authentications count: {}".format(count
)
267 if user_content
.get("username") == "admin":
270 if not self
.config
.get("user_management"):
274 user_content
.get("_admin").get("retry_count")
275 >= self
.config
["max_pwd_attempt"] - 1
278 self
.users_collection
,
279 {"_id": user_content
["_id"]},
280 {"_admin.user_status": "locked"},
283 "Failed to login as the account is locked due to MANY FAILED ATTEMPTS"
289 def authenticate(self
, credentials
, token_info
=None):
291 Authenticate a user using username/password or previous token_info plus project; its creates a new token
293 :param credentials: dictionary that contains:
294 username: name, id or None
295 password: password or None
296 project_id: name, id, or None. If None first found project will be used to get an scope token
297 other items are allowed and ignored
298 :param token_info: previous token_info to obtain authorization
299 :return: the scoped token info or raises an exception. The token is a dictionary with:
300 _id: token string id,
302 project_id: scoped_token project_id,
303 project_name: scoped_token project_name,
304 expires: epoch time when it expires,
309 user
= credentials
.get("username")
310 password
= credentials
.get("password")
311 project
= credentials
.get("project_id")
313 # Try using username/password
315 user_content
= self
.validate_user(user
, password
)
320 "name": "User login",
321 "sourceUserName": user
,
322 "message": "Invalid username/password Project={} Outcome=Failure".format(
328 self
.logger
.exception("{}".format(self
.cef_logger
))
330 "Invalid username/password", http_code
=HTTPStatus
.UNAUTHORIZED
332 if not user_content
.get("_admin", None):
334 "No default project for this user.",
335 http_code
=HTTPStatus
.UNAUTHORIZED
,
338 user_rows
= self
.db
.get_list(
339 self
.users_collection
, {"username": token_info
["username"]}
342 user_content
= user_rows
[0]
344 raise AuthException("Invalid token", http_code
=HTTPStatus
.UNAUTHORIZED
)
347 "Provide credentials: username/password or Authorization Bearer token",
348 http_code
=HTTPStatus
.UNAUTHORIZED
,
350 # Delay upon second request within time window
352 now
- user_content
["_admin"].get("last_token_time", 0)
353 < self
.token_time_window
355 sleep(self
.token_delay
)
356 # user_content["_admin"]["last_token_time"] = now
357 # self.db.replace("users", user_content["_id"], user_content) # might cause race conditions
359 "_admin.last_token_time": now
,
360 "_admin.retry_count": 0,
363 self
.users_collection
,
364 {"_id": user_content
["_id"]},
370 "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
372 for _
in range(0, 32)
375 # projects = user_content.get("projects", [])
376 prm_list
= user_content
.get("project_role_mappings", [])
379 project
= prm_list
[0]["project"] if prm_list
else None
382 "can't find a default project for this user",
383 http_code
=HTTPStatus
.UNAUTHORIZED
,
386 projects
= [prm
["project"] for prm
in prm_list
]
388 proj
= self
.db
.get_one(
389 self
.projects_collection
, {BaseTopic
.id_field("projects", project
): project
}
391 project_name
= proj
["name"]
392 project_id
= proj
["_id"]
393 if project_name
not in projects
and project_id
not in projects
:
395 "project {} not allowed for this user".format(project
),
396 http_code
=HTTPStatus
.UNAUTHORIZED
,
399 # TODO remove admin, this vill be used by roles RBAC
400 if project_name
== "admin":
403 token_admin
= proj
.get("admin", False)
409 if prm
["project"] in [project_id
, project_name
]:
410 role
= self
.db
.get_one(
411 self
.roles_collection
,
412 {BaseTopic
.id_field("roles", prm
["role"]): prm
["role"]},
418 roles_list
.append({"name": rnm
, "id": rid
})
420 rid
= self
.db
.get_one(self
.roles_collection
, {"name": "project_admin"})[
423 roles_list
= [{"name": "project_admin", "id": rid
}]
425 login_count
= user_content
.get("_admin").get("retry_count")
426 last_token_time
= user_content
.get("_admin").get("last_token_time")
430 if self
.config
.get("user_management"):
431 for role
in roles_list
:
432 role_id
= role
.get("id")
433 permission
= self
.db
.get_one(
434 self
.roles_collection
,
435 {BaseTopic
.id_field(self
.roles_collection
, role_id
): role_id
},
437 if permission
.get("permissions")["admin"]:
438 if permission
.get("permissions")["default"]:
445 "expires": now
+ 3600,
448 "project_id": proj
["_id"],
449 "project_name": proj
["name"],
450 "username": user_content
["username"],
451 "user_id": user_content
["_id"],
452 "admin": token_admin
,
454 "login_count": login_count
,
455 "last_login": last_token_time
,
456 "admin_show": admin_show
,
457 "user_show": user_show
,
460 self
.db
.create(self
.tokens_collection
, new_token
)
461 return deepcopy(new_token
)
463 def get_role_list(self
, filter_q
={}):
467 :return: returns the list of roles.
469 return self
.db
.get_list(self
.roles_collection
, filter_q
)
471 def create_role(self
, role_info
):
475 :param role_info: full role info.
476 :return: returns the role id.
477 :raises AuthconnOperationException: if role creation failed.
479 # TODO: Check that role name does not exist ?
481 role_info
["_id"] = rid
482 rid
= self
.db
.create(self
.roles_collection
, role_info
)
485 def delete_role(self
, role_id
):
489 :param role_id: role identifier.
490 :raises AuthconnOperationException: if role deletion failed.
492 rc
= self
.db
.del_one(self
.roles_collection
, {"_id": role_id
})
493 self
.db
.del_list(self
.tokens_collection
, {"roles.id": role_id
})
496 def update_role(self
, role_info
):
500 :param role_info: full role info.
501 :return: returns the role name and id.
502 :raises AuthconnOperationException: if user creation failed.
504 rid
= role_info
["_id"]
505 self
.db
.set_one(self
.roles_collection
, {"_id": rid
}, role_info
)
506 return {"_id": rid
, "name": role_info
["name"]}
508 def create_user(self
, user_info
):
512 :param user_info: full user info.
513 :return: returns the username and id of the user.
515 BaseTopic
.format_on_new(user_info
, make_public
=False)
517 user_info
["_admin"]["salt"] = salt
518 user_info
["_admin"]["user_status"] = "active"
520 if not user_info
["username"] == "admin":
521 if self
.config
.get("user_management"):
522 user_info
["_admin"]["modified"] = present
523 user_info
["_admin"]["password_expire_time"] = present
524 account_expire_time
= present
+ 86400 * self
.config
.get(
525 "account_expire_days"
527 user_info
["_admin"]["account_expire_time"] = account_expire_time
529 user_info
["_admin"]["retry_count"] = 0
530 user_info
["_admin"]["last_token_time"] = present
531 if "password" in user_info
:
532 user_info
["password"] = sha256(
533 user_info
["password"].encode("utf-8") + salt
.encode("utf-8")
535 user_info
["_admin"]["password_history"] = {salt
: user_info
["password"]}
536 # "projects" are not stored any more
537 if "projects" in user_info
:
538 del user_info
["projects"]
539 self
.db
.create(self
.users_collection
, user_info
)
540 return {"username": user_info
["username"], "_id": user_info
["_id"]}
542 def update_user(self
, user_info
):
544 Change the user name and/or password.
546 :param user_info: user info modifications
548 uid
= user_info
["_id"]
549 old_pwd
= user_info
.get("old_password")
550 unlock
= user_info
.get("unlock")
551 renew
= user_info
.get("renew")
552 permission_id
= user_info
.get("system_admin_id")
555 user_data
= self
.db
.get_one(
556 self
.users_collection
, {BaseTopic
.id_field("users", uid
): uid
}
559 salt
= user_data
["_admin"]["salt"]
560 shadow_password
= sha256(
561 old_pwd
.encode("utf-8") + salt
.encode("utf-8")
563 if shadow_password
!= user_data
["password"]:
564 raise AuthconnConflictException(
565 "Incorrect password", http_code
=HTTPStatus
.CONFLICT
571 if not permission_id
:
572 raise AuthconnConflictException(
573 "system_admin_id is the required field to unlock the user",
574 http_code
=HTTPStatus
.CONFLICT
,
577 system_user
= self
.db
.get_one(
578 self
.users_collection
,
581 self
.users_collection
, permission_id
585 mapped_roles
= system_user
.get("project_role_mappings")
586 for role
in mapped_roles
:
587 role_id
= role
.get("role")
588 role_assigned
= self
.db
.get_one(
589 self
.roles_collection
,
590 {BaseTopic
.id_field(self
.roles_collection
, role_id
): role_id
},
592 if role_assigned
.get("permissions")["admin"]:
593 if role_assigned
.get("permissions")["default"]:
594 user_data
["_admin"]["retry_count"] = 0
595 if now
> user_data
["_admin"]["account_expire_time"]:
596 user_data
["_admin"]["user_status"] = "expired"
598 user_data
["_admin"]["user_status"] = "active"
602 raise AuthconnConflictException(
603 "User '{}' does not have the privilege to unlock the user".format(
606 http_code
=HTTPStatus
.CONFLICT
,
612 if not permission_id
:
613 raise AuthconnConflictException(
614 "system_admin_id is the required field to renew the user",
615 http_code
=HTTPStatus
.CONFLICT
,
618 system_user
= self
.db
.get_one(
619 self
.users_collection
,
622 self
.users_collection
, permission_id
626 mapped_roles
= system_user
.get("project_role_mappings")
627 for role
in mapped_roles
:
628 role_id
= role
.get("role")
629 role_assigned
= self
.db
.get_one(
630 self
.roles_collection
,
631 {BaseTopic
.id_field(self
.roles_collection
, role_id
): role_id
},
633 if role_assigned
.get("permissions")["admin"]:
634 if role_assigned
.get("permissions")["default"]:
637 present
+ 86400 * self
.config
["account_expire_days"]
639 user_data
["_admin"]["modified"] = present
640 user_data
["_admin"]["account_expire_time"] = account_expire
642 user_data
["_admin"]["retry_count"]
643 >= self
.config
["max_pwd_attempt"]
645 user_data
["_admin"]["user_status"] = "locked"
647 user_data
["_admin"]["user_status"] = "active"
651 raise AuthconnConflictException(
652 "User '{}' does not have the privilege to renew the user".format(
655 http_code
=HTTPStatus
.CONFLICT
,
657 BaseTopic
.format_on_edit(user_data
, user_info
)
659 usnm
= user_info
.get("username")
661 user_data
["username"] = usnm
662 # If password is given and is not already encripted
663 pswd
= user_info
.get("password")
665 len(pswd
) != 64 or not re
.match("[a-fA-F0-9]*", pswd
)
666 ): # TODO: Improve check?
670 "name": "Change Password",
671 "sourceUserName": user_data
["username"],
672 "message": "Changing Password for user, Outcome=Success",
676 self
.logger
.info("{}".format(self
.cef_logger
))
678 if "_admin" not in user_data
:
679 user_data
["_admin"] = {}
680 if user_data
.get("_admin").get("password_history"):
681 old_pwds
= user_data
.get("_admin").get("password_history")
684 for k
, v
in old_pwds
.items():
685 shadow_password
= sha256(
686 pswd
.encode("utf-8") + k
.encode("utf-8")
688 if v
== shadow_password
:
689 raise AuthconnConflictException(
690 "Password is used before", http_code
=HTTPStatus
.CONFLICT
692 user_data
["_admin"]["salt"] = salt
693 user_data
["password"] = sha256(
694 pswd
.encode("utf-8") + salt
.encode("utf-8")
696 if len(old_pwds
) >= 3:
697 old_pwds
.pop(list(old_pwds
.keys())[0])
698 old_pwds
.update({salt
: user_data
["password"]})
699 user_data
["_admin"]["password_history"] = old_pwds
700 if not user_data
["username"] == "admin":
701 if self
.config
.get("user_management"):
703 if self
.config
.get("pwd_expire_days"):
704 expire
= present
+ 86400 * self
.config
.get("pwd_expire_days")
705 user_data
["_admin"]["modified"] = present
706 user_data
["_admin"]["password_expire_time"] = expire
707 # Project-Role Mappings
708 # TODO: Check that user_info NEVER includes "project_role_mappings"
709 if "project_role_mappings" not in user_data
:
710 user_data
["project_role_mappings"] = []
711 for prm
in user_info
.get("add_project_role_mappings", []):
712 user_data
["project_role_mappings"].append(prm
)
713 for prm
in user_info
.get("remove_project_role_mappings", []):
714 for pidf
in ["project", "project_name"]:
715 for ridf
in ["role", "role_name"]:
717 user_data
["project_role_mappings"].remove(
718 {"role": prm
[ridf
], "project": prm
[pidf
]}
724 idf
= BaseTopic
.id_field("users", uid
)
725 self
.db
.set_one(self
.users_collection
, {idf
: uid
}, user_data
)
726 if user_info
.get("remove_project_role_mappings"):
727 idf
= "user_id" if idf
== "_id" else idf
728 self
.db
.del_list(self
.tokens_collection
, {idf
: uid
})
730 def delete_user(self
, user_id
):
734 :param user_id: user identifier.
735 :raises AuthconnOperationException: if user deletion failed.
737 self
.db
.del_one(self
.users_collection
, {"_id": user_id
})
738 self
.db
.del_list(self
.tokens_collection
, {"user_id": user_id
})
741 def get_user_list(self
, filter_q
=None):
745 :param filter_q: dictionary to filter user list by:
746 name (username is also admitted). If a user id is equal to the filter name, it is also provided
748 :return: returns a list of users.
750 filt
= filter_q
or {}
751 if "name" in filt
: # backward compatibility
752 filt
["username"] = filt
.pop("name")
753 if filt
.get("username") and is_valid_uuid(filt
["username"]):
754 # username cannot be a uuid. If this is the case, change from username to _id
755 filt
["_id"] = filt
.pop("username")
756 users
= self
.db
.get_list(self
.users_collection
, filt
)
760 prms
= user
.get("project_role_mappings")
761 projects
= user
.get("projects")
764 # add project_name and role_name. Generate projects for backward compatibility
766 project_id
= prm
["project"]
767 if project_id
not in project_id_name
:
768 pr
= self
.db
.get_one(
769 self
.projects_collection
,
770 {BaseTopic
.id_field("projects", project_id
): project_id
},
773 project_id_name
[project_id
] = pr
["name"] if pr
else None
774 prm
["project_name"] = project_id_name
[project_id
]
775 if prm
["project_name"] not in projects
:
776 projects
.append(prm
["project_name"])
778 role_id
= prm
["role"]
779 if role_id
not in role_id_name
:
780 role
= self
.db
.get_one(
781 self
.roles_collection
,
782 {BaseTopic
.id_field("roles", role_id
): role_id
},
785 role_id_name
[role_id
] = role
["name"] if role
else None
786 prm
["role_name"] = role_id_name
[role_id
]
787 user
["projects"] = projects
# for backward compatibility
789 # user created with an old version. Create a project_role mapping with role project_admin
790 user
["project_role_mappings"] = []
791 role
= self
.db
.get_one(
792 self
.roles_collection
,
793 {BaseTopic
.id_field("roles", "project_admin"): "project_admin"},
795 for p_id_name
in projects
:
796 pr
= self
.db
.get_one(
797 self
.projects_collection
,
798 {BaseTopic
.id_field("projects", p_id_name
): p_id_name
},
801 "project": pr
["_id"],
802 "project_name": pr
["name"],
803 "role_name": "project_admin",
806 user
["project_role_mappings"].append(prm
)
808 user
["projects"] = []
809 user
["project_role_mappings"] = []
813 def get_project_list(self
, filter_q
={}):
817 :return: returns the list of projects.
819 return self
.db
.get_list(self
.projects_collection
, filter_q
)
821 def create_project(self
, project_info
):
825 :param project: full project info.
826 :return: the internal id of the created project
827 :raises AuthconnOperationException: if project creation failed.
829 pid
= self
.db
.create(self
.projects_collection
, project_info
)
832 def delete_project(self
, project_id
):
836 :param project_id: project identifier.
837 :raises AuthconnOperationException: if project deletion failed.
839 idf
= BaseTopic
.id_field("projects", project_id
)
840 r
= self
.db
.del_one(self
.projects_collection
, {idf
: project_id
})
841 idf
= "project_id" if idf
== "_id" else "project_name"
842 self
.db
.del_list(self
.tokens_collection
, {idf
: project_id
})
845 def update_project(self
, project_id
, project_info
):
847 Change the name of a project
849 :param project_id: project to be changed
850 :param project_info: full project info
852 :raises AuthconnOperationException: if project update failed.
855 self
.projects_collection
,
856 {BaseTopic
.id_field("projects", project_id
): project_id
},