def get(self, *args, **kwargs): # type: (*Any, **Any) -> None group_id = kwargs.get("group_id") # type: Optional[int] name = kwargs.get("name") # type: Optional[str] group = Group.get(self.session, group_id, name) if not group: return self.notfound() # Only members can request permissions if not self.current_user.is_member(group.my_members()): return self.forbidden() args_by_perm = get_grantable_permissions( self.session, settings().restricted_ownership_permissions ) dropdown_form, text_form = GroupPermissionRequest._get_forms(args_by_perm, None) self.render( "group-permission-request.html", dropdown_form=dropdown_form, text_form=text_form, group=group, args_by_perm_json=json.dumps(args_by_perm), dropdown_help=settings().permission_request_dropdown_help, text_help=settings().permission_request_text_help, )
def get(self, *args: Any, **kwargs: Any) -> None: name = self.get_path_argument("name") user = User.get(self.session, name=name) if not user: return self.notfound() if not self.check_access(self.session, self.current_user, user): return self.forbidden() metadata_key = self.get_path_argument("key") if metadata_key == USER_METADATA_SHELL_KEY: return self.redirect("/users/{}/shell".format(user.name)) elif metadata_key == USER_METADATA_GITHUB_USERNAME_KEY: return self.redirect("/github/link_begin/{}".format(user.id)) known_field = metadata_key in settings().metadata_options metadata_item = get_user_metadata_by_key(self.session, user.id, metadata_key) if not metadata_item and not known_field: return self.notfound() form = UserMetadataForm() form.value.choices = settings().metadata_options.get( metadata_key, DEFAULT_METADATA_OPTIIONS) self.render( "user-metadata.html", form=form, user=user, is_enabled=known_field, metadata_key=metadata_key, )
def get(self, *args, **kwargs): # type: (*Any, **Any) -> None group_id = kwargs.get("group_id") # type: Optional[int] name = kwargs.get("name") # type: Optional[str] group = Group.get(self.session, group_id, name) if not group: return self.notfound() # Only members can request permissions if not self.current_user.is_member(group.my_members()): return self.forbidden() args_by_perm = get_grantable_permissions( self.session, settings().restricted_ownership_permissions) dropdown_form, text_form = GroupPermissionRequest._get_forms( args_by_perm, None) self.render( "group-permission-request.html", dropdown_form=dropdown_form, text_form=text_form, group=group, args_by_perm_json=json.dumps(args_by_perm), dropdown_help=settings().permission_request_dropdown_help, text_help=settings().permission_request_text_help, )
def get_current_user(self): # type: () -> Optional[User] username = self.request.headers.get(settings().user_auth_header) if not username: return None # Users must be fully qualified if not re.match("^{}$".format(USERNAME_VALIDATION), username): raise InvalidUser() try: user, created = User.get_or_create(self.session, username=username) if created: logging.info("Created new user %s", username) self.session.commit() # Because the graph doesn't initialize until the updates table # is populated, we need to refresh the graph here in case this # is the first update. self.graph.update_from_db(self.session) except sqlalchemy.exc.OperationalError: # Failed to connect to database or create user, try to reconfigure the db. This invokes # the fetcher to try to see if our URL string has changed. Session.configure(bind=get_db_engine(settings().database)) raise DatabaseFailure() # service accounts are, by definition, not interactive users if user.is_service_account: raise InvalidUser() return user
def get(self, *args, **kwargs): # type: (*Any, **Any) -> None self.render( "help.html", how_to_get_help=settings().how_to_get_help, site_docs=settings().site_docs, grant_perm=get_permission(self.session, PERMISSION_GRANT), create_perm=get_permission(self.session, PERMISSION_CREATE), audit_perm=get_permission(self.session, PERMISSION_AUDITOR), )
def get(self, *args: Any, **kwargs: Any) -> None: # Redirect the user to GitHub to authorize our app. state = binascii.hexlify(os.urandom(16)) self.set_cookie("github-link-state", state, httponly=True) params = { "client_id": settings().github_app_client_id, "state": state, "redirect_uri": "{}/github/link_complete/{}".format(settings().url, kwargs["user_id"]), } self.redirect("https://github.com/login/oauth/authorize?" + urlencode(params))
def post(self, *args: Any, **kwargs: Any) -> None: name = self.get_path_argument("name") user = User.get(self.session, name=name) if not user: return self.notfound() if not self.check_access(self.session, self.current_user, user): return self.forbidden() metadata_key = self.get_path_argument("key") if metadata_key == USER_METADATA_SHELL_KEY: return self.redirect("/users/{}/shell".format(user.name)) elif metadata_key == USER_METADATA_GITHUB_USERNAME_KEY: return self.redirect("/github/link_begin/{}".format(user.id)) known_field = metadata_key in settings().metadata_options metadata_item = get_user_metadata_by_key(self.session, user.id, metadata_key) if not metadata_item and not known_field: return self.notfound() form = UserMetadataForm(self.request.arguments) form.value.choices = settings().metadata_options.get( metadata_key, DEFAULT_METADATA_OPTIIONS) if not form.validate(): return self.render( "user-metadata.html", form=form, user=user, metadata_key=metadata_key, is_enabled=known_field, alerts=self.get_form_alerts(form.errors), ) set_user_metadata(self.session, user.id, metadata_key, form.data["value"]) AuditLog.log( self.session, self.current_user.id, "changed_user_metadata", "Changed {}: {}".format(metadata_key, form.data["value"]), on_user_id=user.id, ) return self.redirect("/users/{}?refresh=yes".format(user.name))
def _build_form(self, data): # type: (Optional[int]) -> Tuple[PermissionRequestForm, Dict[str, List[str]]] """Build the permission request form given the request and POST data. Normally all fields of the form will be editable. But if the URL locks down a specific value for the group, permission, or argument, then the specified fields will display those values and will be grayed out and not editable. """ session = self.session current_user = self.current_user def pairs(seq): # type: (Iterable[str]) -> List[Tuple[str, str]] return [(item, item) for item in seq] form = PermissionRequestForm(data) group_names = { g.groupname for g, e in get_groups_by_user(session, current_user) } args_by_perm = get_grantable_permissions( session, settings().restricted_ownership_permissions) permission_names = {p for p in args_by_perm} group_param = self.get_argument("group", None) if group_param is not None: if group_param not in group_names: raise HTTPError( status_code=404, reason="the group name in the URL is not one you belong to" ) form.group_name.choices = pairs([group_param]) form.group_name.render_kw = {"readonly": "readonly"} form.group_name.data = group_param else: form.group_name.choices = pairs([""] + sorted(group_names)) permission_param = self.get_argument("permission", None) if permission_param is not None: if permission_param not in permission_names: raise HTTPError( status_code=404, reason="an unrecognized permission is specified in the URL" ) form.permission.choices = pairs([permission_param]) form.permission.render_kw = {"readonly": "readonly"} form.permission.data = permission_param else: form.permission.choices = pairs(sorted(permission_names)) argument_param = self.get_argument("argument", "") if argument_param: form.argument.render_kw = {"readonly": "readonly"} form.argument.data = argument_param return form, args_by_perm
def post(self, *args: Any, **kwargs: Any) -> None: name = self.get_path_argument("name") user = User.get(self.session, name=name) if not user: return self.notfound() if not self.check_access(self.session, self.current_user, user): return self.forbidden() form = UserShellForm(self.request.arguments) form.shell.choices = settings().shell if not form.validate(): return self.render("user-shell.html", form=form, user=user, alerts=self.get_form_alerts(form.errors)) set_user_metadata(self.session, user.id, USER_METADATA_SHELL_KEY, form.data["shell"]) AuditLog.log( self.session, self.current_user.id, "changed_shell", "Changed shell: {}".format(form.data["shell"]), on_user_id=user.id, ) return self.redirect("/users/{}?refresh=yes".format(user.name))
def baduser(self, message) -> None: self.set_status(403) self.raise_and_log_exception(HTTPError(403)) how_to_get_help = settings().how_to_get_help self.render("errors/baduser.html", message=message, how_to_get_help=how_to_get_help)
def post(self, *args, **kwargs): # type: (*Any, **Any) -> None user_id = kwargs.get("user_id") # type: Optional[int] name = kwargs.get("name") # type: Optional[str] user = User.get(self.session, user_id, name) if not user: return self.notfound() if not self.check_access(self.session, self.current_user, user): return self.forbidden() form = UserShellForm(self.request.arguments) form.shell.choices = settings().shell if not form.validate(): return self.render( "user-shell.html", form=form, user=user, alerts=self.get_form_alerts(form.errors) ) set_user_metadata(self.session, user.id, USER_METADATA_SHELL_KEY, form.data["shell"]) AuditLog.log( self.session, self.current_user.id, "changed_shell", "Changed shell: {}".format(form.data["shell"]), on_user_id=user.id, ) return self.redirect("/users/{}?refresh=yes".format(user.name))
def post(self, *args, **kwargs): # type: (*Any, **Any) -> None user_id = kwargs.get("user_id") # type: Optional[int] name = kwargs.get("name") # type: Optional[str] user = User.get(self.session, user_id, name) if not user: return self.notfound() if not self.check_access(self.session, self.current_user, user): return self.forbidden() form = UserTokenForm(self.request.arguments) if not form.validate(): return self.render( "user-token-add.html", form=form, user=user, alerts=self.get_form_alerts(form.errors), ) try: token, secret = add_new_user_token( self.session, UserToken(name=form.data["name"], user=user)) self.session.commit() except IntegrityError: self.session.rollback() form.name.errors.append("Name already in use.") return self.render( "user-token-add.html", form=form, user=user, alerts=self.get_form_alerts(form.errors), ) AuditLog.log( self.session, self.current_user.id, "add_token", "Added token: {}".format(token.name), on_user_id=user.id, ) email_context = { "actioner": self.current_user.name, "changed_user": user.name, "action": "added", } send_email( self.session, [user.name], "User token created", "user_tokens_changed", settings(), email_context, ) return self.render("user-token-created.html", token=token, secret=secret)
def baduser(self, message): # type: (str) -> None self.set_status(403) self.raise_and_log_exception(tornado.web.HTTPError(403)) how_to_get_help = settings().how_to_get_help self.render("errors/baduser.html", message=message, how_to_get_help=how_to_get_help)
def post(self, *args, **kwargs): # type: (*Any, **Any) -> None user_id = kwargs.get("user_id") # type: Optional[int] name = kwargs.get("name") # type: Optional[str] user = User.get(self.session, user_id, name) if not user: return self.notfound() if not self.check_access(self.session, self.current_user, user): return self.forbidden() form = UserTokenForm(self.request.arguments) if not form.validate(): return self.render( "user-token-add.html", form=form, user=user, alerts=self.get_form_alerts(form.errors), ) try: token, secret = add_new_user_token( self.session, UserToken(name=form.data["name"], user=user) ) self.session.commit() except IntegrityError: self.session.rollback() form.name.errors.append("Name already in use.") return self.render( "user-token-add.html", form=form, user=user, alerts=self.get_form_alerts(form.errors), ) AuditLog.log( self.session, self.current_user.id, "add_token", "Added token: {}".format(token.name), on_user_id=user.id, ) email_context = { "actioner": self.current_user.name, "changed_user": user.name, "action": "added", } send_email( self.session, [user.name], "User token created", "user_tokens_changed", settings(), email_context, ) return self.render("user-token-created.html", token=token, secret=secret)
def post(self, *args, **kwargs): # type: (*Any, **Any) -> None user_id = kwargs.get("user_id") # type: Optional[int] name = kwargs.get("name") # type: Optional[str] user = User.get(self.session, user_id, name) if not user: return self.notfound() if not self.check_access(self.session, self.current_user, user): return self.forbidden() form = UserPasswordForm(self.request.arguments) if not form.validate(): return self.render( "user-password-add.html", form=form, user=user, alerts=self.get_form_alerts(form.errors), ) pass_name = form.data["name"] password = form.data["password"] try: add_new_user_password(self.session, pass_name, password, user.id) except PasswordAlreadyExists: self.session.rollback() form.name.errors.append("Name already in use.") return self.render( "user-password-add.html", form=form, user=user, alerts=self.get_form_alerts(form.errors), ) AuditLog.log( self.session, self.current_user.id, "add_password", "Added password: {}".format(pass_name), on_user_id=user.id, ) email_context = { "actioner": self.current_user.name, "changed_user": user.name, "pass_name": pass_name, } send_email( self.session, [user.name], "User password created", "user_password_created", settings(), email_context, ) return self.redirect("/users/{}?refresh=yes".format(user.name))
def get(self, *args: Any, **kwargs: Any) -> Iterator[Future]: # Check that the state parameter is correct. state = self.request.query_arguments.get("state", [b""])[-1] expected_state = self.get_cookie("github-link-state", "").encode("utf-8") self.clear_cookie("github-link-state") if not hmac.compare_digest(state, expected_state): self.badrequest() return code = self.get_query_argument("code") # Make sure we're modifying the authenticated user before doing more. user_id = kwargs["user_id"] user = User.get(self.session, user_id) if not user: self.notfound() return if self.current_user.id != user.id: self.forbidden() return github_client = GitHubClient(_get_github_http_client(), settings().http_proxy_host, settings().http_proxy_port) oauth_token = yield github_client.get_oauth_access_token( settings().github_app_client_id, get_plugin_proxy().get_github_app_client_secret(), code, state, ) gh_username = yield github_client.get_username(oauth_token) AuditLog.log( self.session, self.current_user.id, "changed_github_username", "Changed GitHub username: {}".format(gh_username), on_user_id=user.id, ) set_user_metadata(self.session, user.id, USER_METADATA_GITHUB_USERNAME_KEY, gh_username) self.redirect("/users/{}?refresh=yes".format(user.name))
def _build_form(self, data): # type: (Optional[int]) -> Tuple[PermissionRequestForm, Dict[Permission, List[str]]] """Build the permission request form given the request and POST data. Normally all fields of the form will be editable. But if the URL locks down a specific value for the group, permission, or argument, then the specified fields will display those values and will be grayed out and not editable. """ session = self.session current_user = self.current_user def pairs(seq): # type: (Iterable[str]) -> List[Tuple[str, str]] return [(item, item) for item in seq] form = PermissionRequestForm(data) group_names = {g.groupname for g, e in get_groups_by_user(session, current_user)} args_by_perm = get_grantable_permissions( session, settings().restricted_ownership_permissions ) permission_names = {p for p in args_by_perm} group_param = self.get_argument("group", None) if group_param is not None: if group_param not in group_names: raise HTTPError( status_code=404, reason="the group name in the URL is not one you belong to" ) form.group_name.choices = pairs([group_param]) form.group_name.render_kw = {"readonly": "readonly"} else: form.group_name.choices = pairs([""] + sorted(group_names)) permission_param = self.get_argument("permission", None) if permission_param is not None: if permission_param not in permission_names: raise HTTPError( status_code=404, reason="an unrecognized permission is specified in the URL" ) form.permission_name.choices = pairs([permission_param]) form.permission_name.render_kw = {"readonly": "readonly"} else: form.permission_name.choices = pairs([""] + sorted(permission_names)) argument_param = self.get_argument("argument", "") if argument_param: form.argument.render_kw = {"readonly": "readonly"} form.argument.data = argument_param return form, args_by_perm
def get(self, *args: Any, **kwargs: Any) -> None: name = self.get_path_argument("name") user = User.get(self.session, name=name) if not user: return self.notfound() if not self.check_access(self.session, self.current_user, user): return self.forbidden() form = UserShellForm() form.shell.choices = settings().shell self.render("user-shell.html", form=form, user=user)
def prepare(self) -> None: username = self.request.headers.get(settings().user_auth_header) try: user = self.get_or_create_user(username) except InvalidUser as e: self.baduser(str(e)) self.finish() return if user and user.enabled: self.current_user = user else: self.baduser("{} is not an active account".format(username)) self.finish()
def prepare(self): # type: () -> None username = self.request.headers.get(settings().user_auth_header) try: user = self.get_or_create_user(username) except InvalidUser as e: self.baduser(str(e)) self.finish() return if user and user.enabled: self.current_user = user else: self.baduser("{} is not an active account".format(username)) self.finish()
def get(self, *args, **kwargs): # type: (*Any, **Any) -> None user_id = kwargs.get("user_id") # type: Optional[int] name = kwargs.get("name") # type: Optional[str] user = User.get(self.session, user_id, name) if not user: return self.notfound() if not self.check_access(self.session, self.current_user, user): return self.forbidden() form = UserShellForm() form.shell.choices = settings().shell self.render("user-shell.html", form=form, user=user)
def initialize(self, *args: Any, **kwargs: Any) -> None: self.graph = Graph() self.session = self.settings["session"]() # type: Session self.template_engine = self.settings[ "template_engine"] # type: FrontendTemplateEngine self.plugins = get_plugin_proxy() session_factory = SingletonSessionFactory(self.session) self.usecase_factory = create_graph_usecase_factory( settings(), self.plugins, session_factory) if self.get_argument("_profile", False): self.perf_collector = Collector() self.perf_trace_uuid = str(uuid4()) # type: Optional[str] self.perf_collector.start() else: self.perf_collector = None self.perf_trace_uuid = None self._request_start_time = datetime.utcnow()
def post(self, *args, **kwargs): # type: (*Any, **Any) -> None user_id = kwargs.get("user_id") # type: Optional[int] name = kwargs.get("name") # type: Optional[str] key_id = kwargs["key_id"] # type: int user = User.get(self.session, user_id, name) if not user: return self.notfound() if not self.check_access(self.session, self.current_user, user): return self.forbidden() try: key = get_public_key(self.session, user.id, key_id) delete_public_key(self.session, user.id, key_id) except KeyNotFound: return self.notfound() AuditLog.log( self.session, self.current_user.id, "delete_public_key", "Deleted public key: {}".format(key.fingerprint_sha256), on_user_id=user.id, ) email_context = { "actioner": self.current_user.name, "changed_user": user.name, "action": "removed", } send_email( self.session, [user.name], "Public SSH key removed", "ssh_keys_changed", settings(), email_context, ) return self.redirect("/users/{}?refresh=yes".format(user.name))
def get_or_create_user(self, username): # type: (str) -> Optional[User] """Retrieve or create the User object for the authenticated user. This is done in a separate method called by prepare instead of in the magic Tornado get_current_user method because exceptions thrown by the latter are caught by Tornado and not propagated to the caller, and we want to use exceptions to handle invalid users and then return an error page in prepare. """ if not username: return None # Users must be fully qualified if not re.match("^{}$".format(USERNAME_VALIDATION), username): raise InvalidUser("{} does not match {}".format( username, USERNAME_VALIDATION)) # User must exist in the database and be active try: user, created = User.get_or_create(self.session, username=username) if created: logging.info("Created new user %s", username) self.session.commit() # Because the graph doesn't initialize until the updates table # is populated, we need to refresh the graph here in case this # is the first update. self.graph.update_from_db(self.session) except sqlalchemy.exc.OperationalError: # Failed to connect to database or create user, try to reconfigure the db. This invokes # the fetcher to try to see if our URL string has changed. Session.configure(bind=get_db_engine(settings().database)) raise DatabaseFailure() # service accounts are, by definition, not interactive users if user.is_service_account: raise InvalidUser("{} is a service account".format(username)) return user
def initialize(self, *args, **kwargs): # type: (*Any, **Any) -> None self.graph = Graph() self.session = self.settings["session"]() # type: Session self.template_engine = self.settings["template_engine"] # type: FrontendTemplateEngine self.plugins = get_plugin_proxy() session_factory = SingletonSessionFactory(self.session) self.usecase_factory = create_graph_usecase_factory( settings(), self.plugins, session_factory ) if self.get_argument("_profile", False): self.perf_collector = Collector() self.perf_trace_uuid = str(uuid4()) # type: Optional[str] self.perf_collector.start() else: self.perf_collector = None self.perf_trace_uuid = None self._request_start_time = datetime.utcnow() stats.log_rate("requests", 1) stats.log_rate("requests_{}".format(self.__class__.__name__), 1)
def get_or_create_user(self, username): # type: (str) -> Optional[User] """Retrieve or create the User object for the authenticated user. This is done in a separate method called by prepare instead of in the magic Tornado get_current_user method because exceptions thrown by the latter are caught by Tornado and not propagated to the caller, and we want to use exceptions to handle invalid users and then return an error page in prepare. """ if not username: return None # Users must be fully qualified if not re.match("^{}$".format(USERNAME_VALIDATION), username): raise InvalidUser("{} does not match {}".format(username, USERNAME_VALIDATION)) # User must exist in the database and be active try: user, created = User.get_or_create(self.session, username=username) if created: logging.info("Created new user %s", username) self.session.commit() # Because the graph doesn't initialize until the updates table # is populated, we need to refresh the graph here in case this # is the first update. self.graph.update_from_db(self.session) except sqlalchemy.exc.OperationalError: # Failed to connect to database or create user, try to reconfigure the db. This invokes # the fetcher to try to see if our URL string has changed. Session.configure(bind=get_db_engine(settings().database)) raise DatabaseFailure() # service accounts are, by definition, not interactive users if user.is_service_account: raise InvalidUser("{} is a service account".format(username)) return user
def initialize(self, *args, **kwargs): # type: (*Any, **Any) -> None self.graph = Graph() self.session = self.settings["session"]() # type: Session self.template_engine = self.settings[ "template_engine"] # type: FrontendTemplateEngine self.plugins = get_plugin_proxy() session_factory = SingletonSessionFactory(self.session) self.usecase_factory = create_graph_usecase_factory( settings(), self.plugins, session_factory) if self.get_argument("_profile", False): self.perf_collector = Collector() self.perf_trace_uuid = str(uuid4()) # type: Optional[str] self.perf_collector.start() else: self.perf_collector = None self.perf_trace_uuid = None self._request_start_time = datetime.utcnow() stats.log_rate("requests", 1) stats.log_rate("requests_{}".format(self.__class__.__name__), 1) logging.error("initialized")
def post(self, *args, **kwargs): # type: (*Any, **Any) -> None form = AuditCreateForm(self.request.arguments) if not form.validate(): return self.render( "audit-create.html", form=form, alerts=self.get_form_alerts(form.errors) ) if not user_has_permission(self.session, self.current_user, AUDIT_MANAGER): return self.forbidden() # Need to lock this and prevent someone from requesting another set of audits # while this is processing lock = Lock() # Step 1, detect if there are non-completed audits and fail if so. with lock: open_audits = self.session.query(Audit).filter(Audit.complete == False).all() if open_audits: raise Exception("Sorry, there are audits in progress.") ends_at = datetime.strptime(form.data["ends_at"], "%m/%d/%Y") # Step 2, find all audited groups and schedule audits for each. audited_groups = [] for groupname in self.graph.groups: if not self.graph.get_group_details(groupname)["audited"]: continue group = Group.get(self.session, name=groupname) assert group, f"Graph contains nonexistent group {groupname}" audit = Audit(group_id=group.id, ends_at=ends_at) try: audit.add(self.session) self.session.flush() except IntegrityError: self.session.rollback() raise Exception("Failed to start the audit. Please try again.") # Update group with new audit audited_groups.append(group) group.audit_id = audit.id # Step 3, now get all members of this group and set up audit rows for those edges. for member in group.my_members().values(): auditmember = AuditMember(audit_id=audit.id, edge_id=member.edge_id) try: auditmember.add(self.session) except IntegrityError: self.session.rollback() raise Exception("Failed to start the audit. Please try again.") self.session.commit() AuditLog.log( self.session, self.current_user.id, "start_audit", "Started global audit.", category=AuditLogCategory.audit, ) # Calculate schedule of emails, basically we send emails at various periods in advance # of the end of the audit period. schedule_times = [] not_before = datetime.utcnow() + timedelta(1) for days_prior in (28, 21, 14, 7, 3, 1): email_time = ends_at - timedelta(days_prior) email_time.replace(hour=17, minute=0, second=0) if email_time > not_before: schedule_times.append((days_prior, email_time)) # Now send some emails. We do this separately/later to ensure that the audits are all # created. Email notifications are sent multiple times if group audits are still # outstanding. for group in audited_groups: mail_to = [ member.name for member in group.my_users() if GROUP_EDGE_ROLES[member.role] in ("owner", "np-owner") ] send_email( self.session, mail_to, "Group Audit: {}".format(group.name), "audit_notice", settings(), {"group": group.name, "ends_at": ends_at}, ) for days_prior, email_time in schedule_times: send_async_email( self.session, mail_to, "Group Audit: {} - {} day(s) left".format(group.name, days_prior), "audit_notice_reminder", settings(), {"group": group.name, "ends_at": ends_at, "days_left": days_prior}, email_time, async_key="audit-{}".format(group.id), ) return self.redirect("/audits")
def post(self, *args, **kwargs): # type: (*Any, **Any) -> None group_id = kwargs.get("group_id") # type: Optional[int] name = kwargs.get("name") # type: Optional[str] group = Group.get(self.session, group_id, name) if not group or not group.enabled: return self.notfound() members = group.my_members() member_groups = {g for t, g in members if t == "Group"} user_is_member = self._is_user_a_member(group, members) form = GroupJoinForm(self.request.arguments) form.member.choices = self._get_choices(group, member_groups, user_is_member) if not form.validate(): return self.render("group-join.html", form=form, group=group, alerts=self.get_form_alerts(form.errors)) member = self._get_member(form.data["member"]) if not member: return self.render( "group-join.html", form=form, group=group, alerts=[ Alert( "danger", "Unknown user or group: {}".format( form.data["member"])) ], ) fail_message = "This join is denied with this role at this time." try: user_can_join = assert_can_join(group, member, role=form.data["role"]) except UserNotAuditor as e: user_can_join = False fail_message = str(e) if not user_can_join: return self.render( "group-join.html", form=form, group=group, alerts=[ Alert("danger", fail_message, "Audit Policy Enforcement") ], ) if group.canjoin == "nobody": fail_message = "This group cannot be joined at this time." return self.render("group-join.html", form=form, group=group, alerts=[Alert("danger", fail_message)]) if group.require_clickthru_tojoin: if not form.data["clickthru_agreement"]: return self.render( "group-join.html", form=form, group=group, alerts=[ Alert( "danger", "please accept review of the group's description", "Clickthru Enforcement", ) ], ) # We only use the default expiration time if no expiration time was given # This does mean that if a user wishes to join a group with no expiration # (even with an owner's permission) that has an auto expiration, they must # first be accepted to the group and then have the owner edit the user to # have no expiration. expiration = None if form.data["expiration"]: expiration = datetime.strptime(form.data["expiration"], "%m/%d/%Y") elif group.auto_expire: expiration = datetime.utcnow() + group.auto_expire request = group.add_member( requester=self.current_user, user_or_group=member, reason=form.data["reason"], status=GROUP_JOIN_CHOICES[group.canjoin], expiration=expiration, role=form.data["role"], ) self.session.commit() if group.canjoin == "canask": AuditLog.log( self.session, self.current_user.id, "join_group", "{} requested to join with role: {}".format( member.name, form.data["role"]), on_group_id=group.id, ) mail_to = [ user.name for user in group.my_users() if GROUP_EDGE_ROLES[user.role] in ("manager", "owner", "np-owner") ] email_context = { "requester": member.name, "requested_by": self.current_user.name, "request_id": request.id, "group_name": group.name, "reason": form.data["reason"], "expiration": expiration, "role": form.data["role"], "references_header": request.reference_id, } subj = self.render_template("email/pending_request_subj.tmpl", group=group.name, user=self.current_user.name) send_email(self.session, mail_to, subj, "pending_request", settings(), email_context) elif group.canjoin == "canjoin": AuditLog.log( self.session, self.current_user.id, "join_group", "{} auto-approved to join with role: {}".format( member.name, form.data["role"]), on_group_id=group.id, ) else: raise Exception("Need to update the GroupJoin.post audit logging") return self.redirect("/groups/{}?refresh=yes".format(group.name))
def post(self, *args, **kwargs): # type: (*Any, **Any) -> None group_id = kwargs.get("group_id") # type: Optional[int] name = kwargs.get("name") # type: Optional[str] group = Group.get(self.session, group_id, name) if not group: return self.notfound() # Only members can request permissions if not self.current_user.is_member(group.my_members()): return self.forbidden() # check inputs args_by_perm = get_grantable_permissions( self.session, settings().restricted_ownership_permissions ) dropdown_form, text_form = GroupPermissionRequest._get_forms( args_by_perm, self.request.arguments ) argument_type = self.request.arguments.get("argument_type") if argument_type and argument_type[0].decode() == "text": form = text_form elif argument_type and argument_type[0].decode() == "dropdown": form = dropdown_form form.argument.choices = [(a, a) for a in args_by_perm[form.permission_name.data]] else: # someone messing with the form self.log_message( "unknown argument type", group_name=group.name, argument_type=argument_type ) return self.forbidden() if not form.validate(): return self.render( "group-permission-request.html", dropdown_form=dropdown_form, text_form=text_form, group=group, args_by_perm_json=json.dumps(args_by_perm), alerts=self.get_form_alerts(form.errors), dropdown_help=settings().permission_request_dropdown_help, text_help=settings().permission_request_text_help, ) permission = get_permission(self.session, form.permission_name.data) assert permission is not None, "our prefilled permission should exist or we have problems" # save off request try: request = permissions.create_request( self.session, self.current_user, group, permission, form.argument.data, form.reason.data, ) except permissions.RequestAlreadyGranted: alerts = [Alert("danger", "This group already has this permission and argument.")] except permissions.RequestAlreadyExists: alerts = [ Alert( "danger", "Request for permission and argument already exists, please wait patiently.", ) ] except permissions.NoOwnersAvailable: self.log_message( "prefilled perm+arg have no owner", group_name=group.name, permission_name=permission.name, argument=form.argument.data, ) alerts = [ Alert( "danger", "No owners available for requested permission and argument." " If this error persists please contact an adminstrator.", ) ] except UserNotAuditor as e: alerts = [Alert("danger", str(e))] else: alerts = [] if alerts: return self.render( "group-permission-request.html", dropdown_form=dropdown_form, text_form=text_form, group=group, args_by_perm_json=json.dumps(args_by_perm), alerts=alerts, ) else: return self.redirect("/permissions/requests/{}".format(request.id))
def post(self, *args, **kwargs): # type: (*Any, **Any) -> None group_id = kwargs.get("group_id") # type: Optional[int] name = kwargs.get("name") # type: Optional[str] group = Group.get(self.session, group_id, name) if not group: return self.notfound() form = GroupJoinForm(self.request.arguments) form.member.choices = self._get_choices(group) if not form.validate(): return self.render( "group-join.html", form=form, group=group, alerts=self.get_form_alerts(form.errors) ) member = self._get_member(form.data["member"]) if not member: return self.render( "group-join.html", form=form, group=group, alerts=[Alert("danger", "Unknown user or group: {}".format(form.data["member"]))], ) fail_message = "This join is denied with this role at this time." try: user_can_join = assert_can_join(group, member, role=form.data["role"]) except UserNotAuditor as e: user_can_join = False fail_message = str(e) if not user_can_join: return self.render( "group-join.html", form=form, group=group, alerts=[Alert("danger", fail_message, "Audit Policy Enforcement")], ) if group.canjoin == "nobody": fail_message = "This group cannot be joined at this time." return self.render( "group-join.html", form=form, group=group, alerts=[Alert("danger", fail_message)] ) if group.require_clickthru_tojoin: if not form.data["clickthru_agreement"]: return self.render( "group-join.html", form=form, group=group, alerts=[ Alert( "danger", "please accept review of the group's description", "Clickthru Enforcement", ) ], ) # We only use the default expiration time if no expiration time was given # This does mean that if a user wishes to join a group with no expiration # (even with an owner's permission) that has an auto expiration, they must # first be accepted to the group and then have the owner edit the user to # have no expiration. expiration = None if form.data["expiration"]: expiration = datetime.strptime(form.data["expiration"], "%m/%d/%Y") elif group.auto_expire: expiration = datetime.utcnow() + group.auto_expire request = group.add_member( requester=self.current_user, user_or_group=member, reason=form.data["reason"], status=GROUP_JOIN_CHOICES[group.canjoin], expiration=expiration, role=form.data["role"], ) self.session.commit() if group.canjoin == "canask": AuditLog.log( self.session, self.current_user.id, "join_group", "{} requested to join with role: {}".format(member.name, form.data["role"]), on_group_id=group.id, ) mail_to = [ user.name for user in group.my_users() if GROUP_EDGE_ROLES[user.role] in ("manager", "owner", "np-owner") ] email_context = { "requester": member.name, "requested_by": self.current_user.name, "request_id": request.id, "group_name": group.name, "reason": form.data["reason"], "expiration": expiration, "role": form.data["role"], "references_header": request.reference_id, } subj = self.render_template( "email/pending_request_subj.tmpl", group=group.name, user=self.current_user.name ) send_email(self.session, mail_to, subj, "pending_request", settings(), email_context) elif group.canjoin == "canjoin": AuditLog.log( self.session, self.current_user.id, "join_group", "{} auto-approved to join with role: {}".format(member.name, form.data["role"]), on_group_id=group.id, ) else: raise Exception("Need to update the GroupJoin.post audit logging") return self.redirect("/groups/{}?refresh=yes".format(group.name))
def test_github(session, users, http_client, base_url, mocker): # noqa: F811 user = users["*****@*****.**"] assert get_user_metadata_by_key(session, user.id, USER_METADATA_GITHUB_USERNAME_KEY) is None user = User.get(session, name=user.username) fe_url = url(base_url, "/github/link_begin/{}".format(user.id)) mocker.patch.object(settings(), "github_app_client_id", "a-client-id") resp = yield http_client.fetch( fe_url, method="GET", headers={"X-Grouper-User": user.username}, follow_redirects=False, raise_error=False, ) assert resp.code == 302 redir_url = urlparse(resp.headers["Location"]) assert redir_url.netloc == "github.com" assert redir_url.path == "/login/oauth/authorize" query_params = parse_qs(redir_url.query) assert query_params["client_id"] == ["a-client-id"] (state, ) = query_params["state"] assert "github-link-state={}".format(state) in resp.headers["Set-cookie"] assert query_params["redirect_uri"] == [ "http://127.0.0.1:8888/github/link_complete/{}".format(user.id) ] fe_url = url( base_url, "/github/link_complete/{}?code=tempcode&state={}".format( user.id, state)) with pytest.raises(HTTPError) as excinfo: yield http_client.fetch( fe_url, method="GET", headers={ "X-Grouper-User": user.username, "Cookie": "github-link-state=bogus-state" }, ) assert excinfo.value.code == 400 recorder = FakeGitHubHttpClient() proxy_plugin = PluginProxy([SecretPlugin()]) mocker.patch("grouper.fe.handlers.github._get_github_http_client", lambda: recorder) mocker.patch("grouper.fe.handlers.github.get_plugin_proxy", lambda: proxy_plugin) mocker.patch.object(settings(), "http_proxy_host", "proxy-server") mocker.patch.object(settings(), "http_proxy_port", 42) resp = yield http_client.fetch( fe_url, method="GET", headers={ "X-Grouper-User": user.username, "Cookie": "github-link-state=" + state }, ) authorize_request, user_request = recorder.requests assert authorize_request.proxy_host == "proxy-server" assert authorize_request.proxy_port == 42 assert user_request.proxy_host == "proxy-server" assert user_request.proxy_port == 42 authorize_params = parse_qs(authorize_request.body) assert authorize_params[b"code"] == [b"tempcode"] assert authorize_params[b"state"] == [state.encode("ascii")] assert authorize_params[b"client_id"] == [b"a-client-id"] assert authorize_params[b"client_secret"] == [b"client-secret"] assert user_request.headers["Authorization"] == "token a-access-token" assert (get_user_metadata_by_key(session, user.id, USER_METADATA_GITHUB_USERNAME_KEY) is not None) assert (get_user_metadata_by_key( session, user.id, USER_METADATA_GITHUB_USERNAME_KEY).data_value == "zorkian-on-gh") audit_entries = AuditLog.get_entries(session, on_user_id=user.id, action="changed_github_username") assert len(audit_entries) == 1 assert audit_entries[ 0].description == "Changed GitHub username: zorkian-on-gh" fe_url = url(base_url, "/users/{}/github/clear".format(user.username)) resp = yield http_client.fetch(fe_url, method="POST", headers={"X-Grouper-User": user.username}, body=b"") assert resp.code == 200 assert get_user_metadata_by_key(session, user.id, USER_METADATA_GITHUB_USERNAME_KEY) is None audit_entries = AuditLog.get_entries(session, on_user_id=user.id, action="changed_github_username") assert len(audit_entries) == 2 audit_entries.sort(key=operator.attrgetter("id")) assert audit_entries[1].description == "Cleared GitHub link"
def post(self, *args, **kwargs): # type: (*Any, **Any) -> None group_id = kwargs.get("group_id") # type: Optional[int] name = kwargs.get("name") # type: Optional[str] group = Group.get(self.session, group_id, name) if not group: return self.notfound() if not user_can_manage_group(self.session, group, self.current_user): return self.forbidden() members = group.my_members() my_role = user_role(self.current_user, members) form = self.get_form(role=my_role) if not form.validate(): return self.render("group-add.html", form=form, group=group, alerts=self.get_form_alerts(form.errors)) member = get_user_or_group(self.session, form.data["member"]) if member.type == "User" and is_role_user(self.session, member): # For service accounts, we want to always add the group to other groups, not the user member = get_role_user(self.session, user=member).group if not member: form.member.errors.append("User or group not found.") elif (member.type, member.name) in group.my_members(): form.member.errors.append( "User or group is already a member of this group.") elif group.name == member.name: form.member.errors.append( "By definition, this group is a member of itself already.") # Ensure this doesn't violate auditing constraints fail_message = "This join is denied with this role at this time." try: user_can_join = assert_can_join(group, member, role=form.data["role"]) except UserNotAuditor as e: user_can_join = False fail_message = str(e) if not user_can_join: form.member.errors.append(fail_message) if form.member.errors: return self.render("group-add.html", form=form, group=group, alerts=self.get_form_alerts(form.errors)) expiration = None if form.data["expiration"]: expiration = datetime.strptime(form.data["expiration"], "%m/%d/%Y") try: group.add_member( requester=self.current_user, user_or_group=member, reason=form.data["reason"], status="actioned", expiration=expiration, role=form.data["role"], ) except InvalidRoleForMember as e: return self.render("group-add.html", form=form, group=group, alerts=[Alert("danger", str(e))]) self.session.commit() on_user_id = member.id if member.type == "User" else None AuditLog.log( self.session, self.current_user.id, "join_group", "{} added to group with role: {}".format(member.name, form.data["role"]), on_group_id=group.id, on_user_id=on_user_id, ) if member.type == "User": send_email( self.session, [member.name], "Added to group: {}".format(group.name), "request_actioned", settings(), { "group_name": group.name, "actioned_by": self.current_user.name, "reason": form.data["reason"], "expiration": expiration, "role": form.data["role"], }, ) return self.redirect("/groups/{}?refresh=yes".format(group.name))
def post(self, *args, **kwargs): # type: (*Any, **Any) -> None group_id = kwargs.get("group_id") # type: Optional[str] name = kwargs.get("name") # type: Optional[str] group = Group.get(self.session, group_id, name) if not group: return self.notfound() if "@" not in self.request.arguments["name"][0].decode(): self.request.arguments["name"][0] += ( "@" + settings().service_account_email_domain).encode() if not can_create_service_account(self.session, self.current_user, group): return self.forbidden() form = ServiceAccountCreateForm(self.request.arguments) if not form.validate(): return self.render( "service-account-create.html", form=form, group=group, alerts=self.get_form_alerts(form.errors), ) if form.data["name"].split( "@")[-1] != settings().service_account_email_domain: form.name.errors.append( "All service accounts must have a username ending in {}". format(settings().service_account_email_domain)) return self.render( "service-account-create.html", form=form, group=group, alerts=self.get_form_alerts(form.errors), ) try: create_service_account( self.session, self.current_user, form.data["name"], form.data["description"], form.data["machine_set"], group, ) except DuplicateServiceAccount: form.name.errors.append( "A user with name {} already exists".format(form.data["name"])) except BadMachineSet as e: form.machine_set.errors.append(str(e)) if form.name.errors or form.machine_set.errors: return self.render( "service-account-create.html", form=form, group=group, alerts=self.get_form_alerts(form.errors), ) url = "/groups/{}/service/{}?refresh=yes".format( group.name, form.data["name"]) return self.redirect(url)
def post(self, *args: Any, **kwargs: Any) -> None: name = self.get_path_argument("name") group = Group.get(self.session, name=name) if not group or not group.enabled: return self.notfound() members = group.my_members() member_groups = {g for t, g in members if t == "Group"} user_is_member = self._is_user_a_member(group, members) form = GroupJoinForm(self.request.arguments) form.member.choices = self._get_choices(group, member_groups, user_is_member) if not form.validate(): return self.render("group-join.html", form=form, group=group, alerts=self.get_form_alerts(form.errors)) member = self._get_member(form.data["member"]) if not member: return self.render( "group-join.html", form=form, group=group, alerts=[ Alert( "danger", "Unknown user or group: {}".format( form.data["member"])) ], ) try: assert_can_join(group, member, role=form.data["role"]) except UserNotAuditor as e: return self.render( "group-join.html", form=form, group=group, alerts=[Alert("danger", str(e), "Audit Policy Enforcement")], ) if group.canjoin == "nobody": fail_message = "This group cannot be joined at this time." return self.render("group-join.html", form=form, group=group, alerts=[Alert("danger", fail_message)]) if group.require_clickthru_tojoin: if not form.data["clickthru_agreement"]: return self.render( "group-join.html", form=form, group=group, alerts=[ Alert( "danger", "please accept review of the group's description", "Clickthru Enforcement", ) ], ) # We only use the default expiration time if no expiration time was given # This does mean that if a user wishes to join a group with no expiration # (even with an owner's permission) that has an auto expiration, they must # first be accepted to the group and then have the owner edit the user to # have no expiration. expiration = None if form.data["expiration"]: expiration = datetime.strptime(form.data["expiration"], "%m/%d/%Y") elif group.auto_expire: expiration = datetime.utcnow() + group.auto_expire # If the requested role is member, set the status based on the group's canjoin setting, # which automatically actions the request if the group can be joined by anyone and # otherwise sets it pending. # # However, we don't want to let people autojoin as owner or np-owner even to otherwise open # groups, so if the role is not member, force the status to pending. if form.data["role"] == "member": status = GROUP_JOIN_CHOICES[group.canjoin] else: status = "pending" try: request = group.add_member( requester=self.current_user, user_or_group=member, reason=form.data["reason"], status=status, expiration=expiration, role=form.data["role"], ) except InvalidRoleForMember as e: return self.render( "group-join.html", form=form, group=group, alerts=[Alert("danger", str(e), "Invalid Role")], ) self.session.commit() if status == "pending": AuditLog.log( self.session, self.current_user.id, "join_group", "{} requested to join with role: {}".format( member.name, form.data["role"]), on_group_id=group.id, ) mail_to = [ user.name for user in group.my_users() if GROUP_EDGE_ROLES[user.role] in ("manager", "owner", "np-owner") ] email_context = { "requester": member.name, "requested_by": self.current_user.name, "request_id": request.id, "group_name": group.name, "reason": form.data["reason"], "expiration": expiration, "role": form.data["role"], "references_header": request.reference_id, } subj = self.render_template("email/pending_request_subj.tmpl", group=group.name, user=self.current_user.name) send_email(self.session, mail_to, subj, "pending_request", settings(), email_context) elif status == "actioned": AuditLog.log( self.session, self.current_user.id, "join_group", "{} auto-approved to join with role: {}".format( member.name, form.data["role"]), on_group_id=group.id, ) else: raise Exception(f"Unknown join status {status}") return self.redirect("/groups/{}?refresh=yes".format(group.name))
def post(self, *args, **kwargs): # type: (*Any, **Any) -> None group_id = kwargs.get("group_id") # type: Optional[int] name = kwargs.get("name") # type: Optional[str] group = Group.get(self.session, group_id, name) if not group: return self.notfound() if not user_can_manage_group(self.session, group, self.current_user): return self.forbidden() members = group.my_members() my_role = user_role(self.current_user, members) form = self.get_form(role=my_role) if not form.validate(): return self.render( "group-add.html", form=form, group=group, alerts=self.get_form_alerts(form.errors) ) member = get_user_or_group(self.session, form.data["member"]) if member.type == "User" and is_role_user(self.session, member): # For service accounts, we want to always add the group to other groups, not the user member = get_role_user(self.session, user=member).group if not member: form.member.errors.append("User or group not found.") elif (member.type, member.name) in group.my_members(): form.member.errors.append("User or group is already a member of this group.") elif group.name == member.name: form.member.errors.append("By definition, this group is a member of itself already.") # Ensure this doesn't violate auditing constraints fail_message = "This join is denied with this role at this time." try: user_can_join = assert_can_join(group, member, role=form.data["role"]) except UserNotAuditor as e: user_can_join = False fail_message = str(e) if not user_can_join: form.member.errors.append(fail_message) if form.member.errors: return self.render( "group-add.html", form=form, group=group, alerts=self.get_form_alerts(form.errors) ) expiration = None if form.data["expiration"]: expiration = datetime.strptime(form.data["expiration"], "%m/%d/%Y") try: group.add_member( requester=self.current_user, user_or_group=member, reason=form.data["reason"], status="actioned", expiration=expiration, role=form.data["role"], ) except InvalidRoleForMember as e: return self.render( "group-add.html", form=form, group=group, alerts=[Alert("danger", str(e))] ) self.session.commit() on_user_id = member.id if member.type == "User" else None AuditLog.log( self.session, self.current_user.id, "join_group", "{} added to group with role: {}".format(member.name, form.data["role"]), on_group_id=group.id, on_user_id=on_user_id, ) if member.type == "User": send_email( self.session, [member.name], "Added to group: {}".format(group.name), "request_actioned", settings(), { "group_name": group.name, "actioned_by": self.current_user.name, "reason": form.data["reason"], "expiration": expiration, "role": form.data["role"], }, ) return self.redirect("/groups/{}?refresh=yes".format(group.name))
def get_user_view_template_vars(session, actor, user, graph): # type: (Session, User, User, GroupGraph) -> Dict[str, Any] # TODO(cbguder): get around circular dependencies from grouper.fe.handlers.user_disable import UserDisable from grouper.fe.handlers.user_enable import UserEnable ret = {} # type: Dict[str, Any] if user.is_service_account: ret["can_control"] = can_manage_service_account( session, user.service_account, actor) or user_is_user_admin( session, actor) ret["can_disable"] = ret["can_control"] ret["can_enable"] = user_is_user_admin(session, actor) ret["can_enable_preserving_membership"] = user_is_user_admin( session, actor) ret["account"] = user.service_account else: ret["can_control"] = user.name == actor.name or user_is_user_admin( session, actor) ret["can_disable"] = UserDisable.check_access(session, actor, user) ret["can_enable_preserving_membership"] = UserEnable.check_access( session, actor, user) ret["can_enable"] = UserEnable.check_access_without_membership( session, actor, user) if user.id == actor.id: ret["num_pending_group_requests"] = user_requests_aggregate( session, actor).count() _, ret["num_pending_perm_requests"] = get_requests(session, status="pending", limit=1, offset=0, owner=actor) else: ret["num_pending_group_requests"] = None ret["num_pending_perm_requests"] = None try: user_md = graph.get_user_details(user.name) except NoSuchUser: # Either user is probably very new, so they have no metadata yet, or # they're disabled, so we've excluded them from the in-memory graph. user_md = {} shell_metadata = get_user_metadata_by_key(session, user.id, USER_METADATA_SHELL_KEY) ret["shell"] = shell_metadata.data_value if shell_metadata else "No shell configured" github_username = get_user_metadata_by_key( session, user.id, USER_METADATA_GITHUB_USERNAME_KEY) ret["github_username"] = github_username.data_value if github_username else "(Unset)" addl_metadata = get_user_metadata( session, user.id, exclude=[USER_METADATA_SHELL_KEY, USER_METADATA_GITHUB_USERNAME_KEY]) ret["addl_metadata"] = addl_metadata if addl_metadata else [] known_metadata_fields = set(settings().metadata_options.keys()) set_metadata_fields = {md.data_key for md in addl_metadata} ret["unset_metadata"] = known_metadata_fields - set_metadata_fields ret["open_audits"] = user_open_audits(session, user) group_edge_list = get_groups_by_user(session, user) if user.enabled else [] ret["groups"] = [{ "name": g.name, "type": "Group", "role": ge._role } for g, ge in group_edge_list] ret["passwords"] = user_passwords(session, user) ret["public_keys"] = get_public_keys_of_user(session, user.id) ret["log_entries"] = get_log_entries_by_user(session, user) ret["user_tokens"] = user.tokens if user.is_service_account: service_account = user.service_account ret["permissions"] = service_account_permissions( session, service_account) else: ret["permissions"] = user_md.get("permissions", []) for permission in ret["permissions"]: permission["granted_on"] = datetime.fromtimestamp( permission["granted_on"]) return ret
def forbidden(self): # type: () -> None self.set_status(403) self.raise_and_log_exception(tornado.web.HTTPError(403)) self.render("errors/forbidden.html", how_to_get_help=settings().how_to_get_help)
def post(self, *args, **kwargs): # type: (*Any, **Any) -> None user_id = kwargs.get("user_id") # type: Optional[int] name = kwargs.get("name") # type: Optional[str] user = User.get(self.session, user_id, name) if not user: return self.notfound() if not self.check_access(self.session, self.current_user, user): return self.forbidden() form = PublicKeyForm(self.request.arguments) if not form.validate(): return self.render( "public-key-add.html", form=form, user=user, alerts=self.get_form_alerts(form.errors), ) try: pubkey = public_key.add_public_key(self.session, user, form.data["public_key"]) except public_key.DuplicateKey: form.public_key.errors.append("Key already in use. Public keys must be unique.") except public_key.PublicKeyParseError: form.public_key.errors.append("Public key appears to be invalid.") except public_key.BadPublicKey as e: form.public_key.errors.append(str(e)) if form.public_key.errors: return self.render( "public-key-add.html", form=form, user=user, alerts=self.get_form_alerts(form.errors), ) AuditLog.log( self.session, self.current_user.id, "add_public_key", "Added public key: {}".format(pubkey.fingerprint_sha256), on_user_id=user.id, ) email_context = { "actioner": self.current_user.name, "changed_user": user.name, "action": "added", } send_email( self.session, [user.name], "Public SSH key added", "ssh_keys_changed", settings(), email_context, ) return self.redirect("/users/{}?refresh=yes".format(user.name))
def post(self, *args, **kwargs): # type: (*Any, **Any) -> None request_id = kwargs["request_id"] # type: int group_id = kwargs.get("group_id") # type: Optional[int] name = kwargs.get("name") # type: Optional[str] group = Group.get(self.session, group_id, name) if not group: return self.notfound() members = group.my_members() my_role = user_role(self.current_user, members) if my_role not in ("manager", "owner", "np-owner"): return self.forbidden() request = self.session.query(Request).filter_by(id=request_id).scalar() if not request: return self.notfound() on_behalf = get_on_behalf_by_request(self.session, request) form = GroupRequestModifyForm(self.request.arguments) form.status.choices = self._get_choices(request.status) updates = request.my_status_updates() if not form.status.choices: alerts = [Alert("info", "Request has already been processed")] return self.render( "group-request-update.html", group=group, request=request, on_behalf=on_behalf, members=members, form=form, alerts=alerts, statuses=REQUEST_STATUS_CHOICES, updates=updates, ) if not form.validate(): return self.render( "group-request-update.html", group=group, request=request, on_behalf=on_behalf, members=members, form=form, alerts=self.get_form_alerts(form.errors), statuses=REQUEST_STATUS_CHOICES, updates=updates, ) # We have to test this here, too, to ensure that someone can't sneak in with a pending # request that used to be allowed. if form.data["status"] != "cancelled": fail_message = "This join is denied with this role at this time." try: user_can_join = assert_can_join( request.requesting, on_behalf, role=request.edge.role ) except UserNotAuditor as e: user_can_join = False fail_message = str(e) if not user_can_join: return self.render( "group-request-update.html", group=group, request=request, on_behalf=on_behalf, members=members, form=form, statuses=REQUEST_STATUS_CHOICES, updates=updates, alerts=[Alert("danger", fail_message, "Audit Policy Enforcement")], ) request.update_status(self.current_user, form.data["status"], form.data["reason"]) self.session.commit() AuditLog.log( self.session, self.current_user.id, "update_request", "Updated request to status: {}".format(form.data["status"]), on_group_id=group.id, on_user_id=request.requester.id, ) edge = self.session.query(GroupEdge).filter_by(id=request.edge_id).one() approver_mail_to = [ user.name for user in group.my_approver_users() if user.name != self.current_user.name and user.name != request.requester.username ] subj = "Re: " + self.render_template( "email/pending_request_subj.tmpl", group=group.name, user=request.requester.username ) send_email( self.session, approver_mail_to, subj, "approver_request_updated", settings(), { "group_name": group.name, "requester": request.requester.username, "changed_by": self.current_user.name, "status": form.data["status"], "role": edge.role, "reason": form.data["reason"], "references_header": request.reference_id, }, ) if form.data["status"] == "actioned": send_email( self.session, [request.requester.name], "Added to group: {}".format(group.groupname), "request_actioned", settings(), { "group_name": group.name, "actioned_by": self.current_user.name, "reason": form.data["reason"], "expiration": edge.expiration, "role": edge.role, }, ) elif form.data["status"] == "cancelled": send_email( self.session, [request.requester.name], "Request to join cancelled: {}".format(group.groupname), "request_cancelled", settings(), { "group_name": group.name, "cancelled_by": self.current_user.name, "reason": form.data["reason"], "expiration": edge.expiration, "role": edge.role, }, ) # No explicit refresh because handler queries SQL. if form.data["redirect_aggregate"]: return self.redirect("/user/requests") else: return self.redirect("/groups/{}/requests".format(group.name))
def forbidden(self) -> None: self.set_status(403) self.raise_and_log_exception(HTTPError(403)) self.render("errors/forbidden.html", how_to_get_help=settings().how_to_get_help)
def post(self, *args, **kwargs): # type: (*Any, **Any) -> None form = AuditCreateForm(self.request.arguments) if not form.validate(): return self.render( "audit-create.html", form=form, alerts=self.get_form_alerts(form.errors) ) if not user_has_permission(self.session, self.current_user, AUDIT_MANAGER): return self.forbidden() # Step 1, detect if there are non-completed audits and fail if so. open_audits = self.session.query(Audit).filter(Audit.complete == False).all() if open_audits: raise Exception("Sorry, there are audits in progress.") ends_at = datetime.strptime(form.data["ends_at"], "%m/%d/%Y") # Step 2, find all audited groups and schedule audits for each. audited_groups = [] for groupname in self.graph.groups: if not self.graph.get_group_details(groupname)["audited"]: continue group = Group.get(self.session, name=groupname) audit = Audit(group_id=group.id, ends_at=ends_at) try: audit.add(self.session) self.session.flush() except IntegrityError: self.session.rollback() raise Exception("Failed to start the audit. Please try again.") # Update group with new audit audited_groups.append(group) group.audit_id = audit.id # Step 3, now get all members of this group and set up audit rows for those edges. for member in itervalues(group.my_members()): auditmember = AuditMember(audit_id=audit.id, edge_id=member.edge_id) try: auditmember.add(self.session) except IntegrityError: self.session.rollback() raise Exception("Failed to start the audit. Please try again.") self.session.commit() AuditLog.log( self.session, self.current_user.id, "start_audit", "Started global audit.", category=AuditLogCategory.audit, ) # Calculate schedule of emails, basically we send emails at various periods in advance # of the end of the audit period. schedule_times = [] not_before = datetime.utcnow() + timedelta(1) for days_prior in (28, 21, 14, 7, 3, 1): email_time = ends_at - timedelta(days_prior) email_time.replace(hour=17, minute=0, second=0) if email_time > not_before: schedule_times.append((days_prior, email_time)) # Now send some emails. We do this separately/later to ensure that the audits are all # created. Email notifications are sent multiple times if group audits are still # outstanding. for group in audited_groups: mail_to = [ member.name for member in group.my_users() if GROUP_EDGE_ROLES[member.role] in ("owner", "np-owner") ] send_email( self.session, mail_to, "Group Audit: {}".format(group.name), "audit_notice", settings(), {"group": group.name, "ends_at": ends_at}, ) for days_prior, email_time in schedule_times: send_async_email( self.session, mail_to, "Group Audit: {} - {} day(s) left".format(group.name, days_prior), "audit_notice_reminder", settings(), {"group": group.name, "ends_at": ends_at, "days_left": days_prior}, email_time, async_key="audit-{}".format(group.id), ) return self.redirect("/audits")