Bivio::Biz::Model::RealmOwner
# Copyright (c) 1999-2023 bivio Software, Inc. All rights reserved. package Bivio::Biz::Model::RealmOwner; use strict; use Bivio::Base 'Biz.PropertyModel'; my($_DT) = b_use('Type.DateTime'); my($_RN) = b_use('Type.RealmName'); my($_PI) = b_use('Type.PrimaryId'); my($_P) = b_use('Type.Password'); my($_RT) = b_use('Auth.RealmType'); my($_HOME_TASK_MAP) = { map({ $_ => b_use('Agent.TaskId')->from_name($_->get_name . '_HOME'), } (grep($_->equals_by_name(qw(GENERAL)) ? 0 : 1, $_RT->get_non_zero_list))), }; sub create { my($self, $values) = @_; # Sets I<creation_date_time>, I<password> (to invalid), # I<display_name>, I<name> if not set, downcases I<name>, then calls SUPER. $values->{name} = substr($values->{realm_type}->get_name, 0, 1) . $values->{realm_id} unless defined($values->{name}); $values->{name} = $_RN->process_name($values->{name}); $values->{display_name} = $values->{name} unless defined($values->{display_name}); $values->{creation_date_time} ||= $_DT->now; $values->{password} = $_P->INVALID unless defined($values->{password}); return shift->SUPER::create(@_); } sub format_email { my($proto, $model, $model_prefix) = shift->internal_get_target(@_); # Returns fully-qualified email address for this realm or '' if the # realm is an offline user. # # See L<format_name|"format_name"> for params. my($name) = $proto->format_name($model, $model_prefix); return $name ? $model->get_request->format_email($name) : ''; } sub format_http { my($proto, $model, $model_prefix) = shift->internal_get_target(@_); # Returns the absolute URL (with http) to access (the root of) this realm. # # HACK! # # See L<format_name|"format_name"> for params. return $proto->format_uri($model, $model_prefix, {require_absolute => 1}); } sub format_mailto { my($proto, $model, $model_prefix) = shift->internal_get_target(@_); # Returns email address with C<mailto:> prefix. # # See L<format_name|"format_name"> for params. return $model->get_request->format_mailto($proto->format_email( $model, $model_prefix)); } sub format_name { my($proto, $model, $model_prefix) = shift->internal_get_target(@_); # Returns the name formatted for display. Accounting offline users # return ''. # # In the second form, I<model> is used to get the values, not I<self>. # Other Models can declare a method of the form: # # sub format_name { # my($self) = shift; # Bivio::Biz::Model::RealmOwner->format($self, 'RealmOwner.', @_); # } return $_RN->to_string( $model->get($model_prefix . 'name')); } sub format_uri { my($proto, $model, $model_prefix) = shift->internal_get_target(@_); my($name) = $proto->format_name($model, $model_prefix); b_die($model->get($model_prefix . 'name'), ': must not be offline user') unless $name; my($task) = $_HOME_TASK_MAP->{$model->get($model_prefix . 'realm_type')}; b_die($model->get($model_prefix . 'name'), ', ', $model->get($model_prefix . 'realm_type'), ': invalid realm type') unless $task; return $model->get_request->format_uri({ task_id => $task, realm => $name, query => undef, path_info => undef, }); } sub has_valid_password { my($self) = @_; # Returns true if self's password is valid. return $_P->is_valid($self->get('password')); } sub init_db { my($self) = @_; # Initializes database with default realms. The default realms # have special realm_ids. foreach my $rt ($_RT->get_non_zero_list) { $self->init_realm_type($rt); } return; } sub init_realm_type { my($self, $rt) = @_; # Adds I<rt> to the database. return $self->create({ name => $rt->as_default_owner_name, realm_id => $rt->as_default_owner_id, realm_type => $rt, }); } sub internal_initialize { # B<FOR INTERNAL USE ONLY> return { version => 1, table_name => 'realm_owner_t', columns => { realm_id => ['PrimaryId', 'PRIMARY_KEY'], name => ['RealmName', 'NOT_NULL_UNIQUE'], password => ['Password', 'NOT_NULL'], realm_type => [b_use('Auth.RealmType'), 'NOT_NULL'], display_name => ['DisplayName', 'NOT_NULL'], creation_date_time => ['DateTime', 'NOT_NULL'], }, auth_id => 'realm_id', # prevent circular dependency, handled by overridden unsafe_get_model() # other => [ # [qw(realm_id Club.club_id User.user_id)], # ], }; } sub invalidate_password { my($self) = @_; $self->update({password => $_P->INVALID}); return; } sub is_auth_realm { my($proto, $model, $model_prefix) = shift->internal_get_target(@_); # Returns true if the current row is the request's auth_realm. my($auth_id) = $model->get_request->get('auth_id'); return 0 unless $auth_id; return $model->get($model_prefix . 'realm_id') eq $auth_id ? 1 : 0; } sub is_auth_user { my($proto, $model, $model_prefix) = shift->internal_get_target(@_); # Returns true if the current row is the request's auth_user. my($auth_user) = $model->get_request->get('auth_user'); return 0 unless $auth_user; return $model->get($model_prefix . 'realm_id') eq $auth_user->get('realm_id') ? 1 : 0; } sub is_default { my($proto, $model, $model_prefix) = shift->internal_get_target(@_); # Returns true if the realm is one of the default realms (general, # user, club). # Default realms have ids same as their types as_int. return $model->get($model_prefix . 'realm_type')->as_int eq $model->get($model_prefix . 'realm_id') ? 1 : 0; } sub is_locked_out { my($proto, $model, $model_prefix) = shift->internal_get_target(@_); return $model->new_other('LoginAttempt')->unauth_load_last_locked_out($model->get('realm_id')); } sub is_name_eq_email { my(undef, $req, $name, $email) = @_; # If I<name> points to I<email>, returns true. Caller should # put error C<EMAIL_LOOP> on the email. If I<name> or I<email> # C<undef>, returns false. return 0 unless defined($name) && defined($email); my($mail_host) = b_use('UI.Facade')->get_value('mail_host', $req); #TODO: ANY OTHER mail_host aliases? return $email eq $name . '@' . $mail_host || $email eq $name . '@www.' . $mail_host; } sub is_offline_user { my($proto, $model, $model_prefix) = shift->internal_get_target(@_); # Returns true if is a offline realm. # # See L<format_name|"format_name"> for params. return $_RN->is_offline( $model->get($model_prefix . 'name')); } sub maybe_upgrade_password { my($self, $clear_text) = @_; return $self if $self->require_otp || !$_P->needs_upgrade($self->get('password')); return $self->update_password($clear_text); } sub require_otp { my($self) = @_; return $self->get_field_type('password')->is_otp($self->get('password')); } sub unauth_delete_realm { my($self, $query) = @_; $self->unauth_load_or_die($query) if $query; $self->new_other( $self->get('realm_type')->as_property_model_class_name, )->unauth_delete_realm($self); return; } sub unauth_load_and_get_id { return shift->unauth_load_by_email_id_or_name_or_die(@_)->get('realm_id'); } sub unauth_load_by_email { my($self, $email, @query) = @_; # Tries to load this realm using I<email> and any other I<query> parameters, # e.g. (realm_type, Bivio::Auth::RealmType->USER()). # # I<email> is interpreted as follows: # # # * # # An C<Bivio::Biz::Model::Email> is loaded with I<email>. If found, # loads the I<realm_id> of the model. # # * # # Parsed for the I<mail_host> associated with this request. # If it matches, the mailhost is stripped and the (syntactically # valid realm) name is used to find a realm owner. # # * # # Returns false. my($query) = @query == 1 ? ref($query[0]) eq 'HASH' ? $query[0] : b_die(@query, ': query not a hash') : {@query}; # Emails are always lower case $email = lc($email); # Load the email. Return the result of the next unauth_load, just in case my($em) = $self->new_other('Email'); return $self->unauth_load({%$query, realm_id => $em->get('realm_id')}) if $em->unauth_load({email => $email}); return unless b_use('UI.Facade')->is_fully_initialized; # Strip off @mail_host and validate resulting name my($mail_host) = '@' . b_use('UI.Facade')->get_value('mail_host', $self->req); return 0 unless $email =~ s/\Q$mail_host\E$//i; # Is it a valid user/club? return $self->unauth_load({ %$query, name => $_RN->process_name($email), }); } sub unauth_load_by_email_id_or_name { my($self, $email_id_or_name) = @_; # If email_id_or_name has an '@', will try to unauth_load_by_email. # Otherwise, tries to load by id or name. return $email_id_or_name =~ /@/ ? $self->unauth_load_by_email($email_id_or_name) : _unauth_load($self, $email_id_or_name, {}); } sub unauth_load_by_email_id_or_name_or_die { my($self, $email_id_or_name) = @_; return shift->unauth_load_by_email_id_or_name(@_) ? $self : $self->throw_die(MODEL_NOT_FOUND => {entity => $email_id_or_name}); } sub unauth_load_by_id_or_name_or_die { my($self, $id_or_name, $realm_type) = @_; # Loads I<id_or_name> or dies with NOT_FOUND. If I<realm_type> is specified, further qualifies the query. _unauth_load($self, $id_or_name, $realm_type ? {realm_type => $_RT->from_any($realm_type)} : {}, 1, ); return $self; } sub unauth_load_by_name_and_type_or_die { my($self, $name, $type) = @_; $type = $_RT->from_any($type) unless ref($type); $self->throw_die(MODEL_NOT_FOUND => {entity => $name, realm_type => $type}) unless $self->unauth_load({ name => $name, realm_type => $type, }); return $self; } sub unsafe_get_model { my($self, $name) = @_; # Overridden to support getting the related User or Club. # For backward compatibility. if ($name eq 'User' || $name eq 'Club') { my($model) = $self->new_other($name); $model->unauth_load({ lc($name).'_id' => $self->get('realm_id'), }); return $model; } return shift->SUPER::unsafe_get_model(@_); } sub update { my($self, $values) = @_; if ($self->require_otp && defined($values->{password}) && $values->{password} ne $self->get('password'), ) { my($otp) = $self->new_other('OTP'); $otp->delete unless $otp->unauth_load({user_id => $self->get('realm_id')}); } return shift->SUPER::update(@_); } sub update_password { my($self, $clear_text) = @_; return $self->update({ password => $_P->encrypt($clear_text) }); } sub validate_login { my($self, $login) = @_; return 'NOT_FOUND' unless $self->unauth_load_by_email_id_or_name($login); return $self->validate_login_for_self; } sub validate_login_for_self { my($self) = @_; my($err) = $self->is_offline_user ? 'is_offline_user' : $self->is_default ? 'is_default' : $self->get('realm_type') != $_RT->USER ? ('realm_type is ' . $self->get('realm_type')->get_name) : !$self->has_valid_password ? 'not has_valid_password' : return; b_warn($err, ' owner=', $self); return 'NOT_FOUND'; } sub validate_password { my($self, $clear_text) = @_; b_die('missing password') unless defined($clear_text) && length($clear_text); my($t) = _canonicalize_for_weak_password($clear_text); return 'WEAK_PASSWORD' if $t eq _canonicalize_for_weak_password($self->get('display_name')) || $t eq _canonicalize_for_weak_password($self->get('name')) || $t eq $self->get('realm_id') || _similar_to_email($self, $t); return; } sub _canonicalize_for_weak_password { my($value) = @_; $value =~ s/[^a-z0-9]//ig; $value = lc($value); return $value; } sub _similar_to_email { my($self, $canonical_value) = @_; my($similar) = 0; $self->new_other('Email')->set_ephemeral->do_iterate(sub { my($it) = @_; my($email) = _canonicalize_for_weak_password($it->get('email')); if (index($email, $canonical_value) >= 0 || index($canonical_value, $email) >= 0) { $similar = 1; return 0; } return 1; }, 'unauth_iterate_start', {realm_id => $self->get('realm_id')}); return $similar; } sub _unauth_load { my($self, $id_or_name, $query, $want_die) = @_; if ($_PI->is_valid($id_or_name)) { $query->{realm_id} = $id_or_name; return 1 if $self->unauth_load($query); } $query->{name} = $_RN->process_name($id_or_name); return 1 if $self->unauth_load($query); if ($id_or_name =~ /^\d+$/) { delete($query->{name}); $query->{realm_id} = $id_or_name; if ($self->unauth_load($query)) { # b_use('IO.Alert')->warn_deprecated( # 'use the RealmType name to load default Realms'); return 1; } } $self->throw_die(MODEL_NOT_FOUND => $query) if $want_die; return 0; } 1;