1 -- Regano database function definitions
3 -- Uses PostgreSQL extensions.
5 -- These functions are intended to be called from the Web UI or other frontend.
7 -- Regano is a domain registration system for OpenNIC TLDs written in
8 -- Perl. This file is part of Regano.
10 -- Regano may be distributed under the same terms as Perl itself. Of
11 -- particular importance, note that while regano is distributed in the
12 -- hope that it will be useful, there is NO WARRANTY OF ANY KIND
13 -- WHATSOEVER WHETHER EXPLICIT OR IMPLIED.
16 -- The type definitions in db_types.sql must already be installed.
17 -- The table definitions in db_tables.sql must already be installed.
18 -- The function definitions in db_functions.sql must already be installed.
19 -- The configuration in db_config.sql must be loaded for these to actually work.
21 -- Inquire about the status of a domain.
22 CREATE OR REPLACE FUNCTION regano_api.domain_status
24 RETURNS regano.domain_status AS $$
28 domain_term CONSTANT interval NOT NULL
29 := (regano.config_get('domain/term')).interval;
31 max_expired_age CONSTANT interval NOT NULL
32 := (regano.config_get('domain/grace_period')).interval;
33 max_pending_age CONSTANT interval NOT NULL
34 := (regano.config_get('domain/pend_term')).interval;
36 active_domain regano.domains%ROWTYPE;
38 primary_label regano.dns_label;
42 FROM regano.bailiwicks
43 WHERE lower(domain_tail) = lower(name)
44 OR lower(domain_tail) = '.'||lower(name);
49 primary_label := substring(name from '^([^.]+)[.]');
50 tail := substring(name from '^[^.]+([.].+[.])$');
52 PERFORM * FROM regano.bailiwicks WHERE lower(domain_tail) = lower(tail);
57 PERFORM * FROM regano.reserved_domains
58 WHERE domain_name = lower(primary_label);
63 -- clean up pending domains, then check if the requested domain is pending
64 DELETE FROM regano.pending_domains WHERE start < (now() - max_pending_age);
65 PERFORM * FROM regano.pending_domains
66 WHERE lower(domain_name) = lower(primary_label)
67 AND lower(domain_tail) = lower(tail);
72 -- automatically renew system domains
74 SET expiration = now() + domain_term
75 WHERE domain_name IN ('@', '@@')
76 AND expiration < now();
78 -- clean up expired domains, then check if the requested domain is active
79 DELETE FROM regano.domains WHERE expiration < (now() - max_expired_age);
80 SELECT * INTO active_domain
82 WHERE (lower(primary_label) = lower(domain_name))
83 AND (lower(tail) = lower(domain_tail));
86 IF now() < active_domain.expiration THEN
95 $$ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER;
96 ALTER FUNCTION regano_api.domain_status (regano.dns_fqdn)
99 -- Inquire why a domain is reserved.
100 CREATE OR REPLACE FUNCTION regano_api.domain_reservation_reason
103 SELECT CASE WHEN regano_api.domain_status($1) <> 'RESERVED' THEN NULL
105 FROM regano.reserved_domains
106 WHERE domain_name = substring($1 from '^([^.]+)[.]')
107 $$ LANGUAGE SQL STABLE STRICT SECURITY DEFINER;
108 ALTER FUNCTION regano_api.domain_reservation_reason (regano.dns_fqdn)
111 -- Inquire how a domain is handled.
112 CREATE OR REPLACE FUNCTION regano_api.domain_mode
114 RETURNS regano.domain_mode AS $$
118 active_domain regano.domains%ROWTYPE;
120 SELECT * INTO active_domain
122 WHERE lower(name) = lower(domain_name||domain_tail);
123 IF NOT FOUND OR now() > active_domain.expiration THEN
127 PERFORM * FROM regano.domain_records
128 WHERE domain_id = active_domain.id AND seq_no = 0 AND type = 'SOA';
133 PERFORM * FROM regano.domain_records r
134 WHERE domain_id = active_domain.id AND r.name = '@' AND type = 'NS';
141 $$ LANGUAGE plpgsql STABLE STRICT SECURITY DEFINER;
142 ALTER FUNCTION regano_api.domain_mode (regano.dns_fqdn)
145 -- Inquire when a domain was last updated.
146 CREATE OR REPLACE FUNCTION regano_api.domain_last_update
148 RETURNS timestamp with time zone AS $$
152 timestamp timestamp with time zone;
154 SELECT last_update INTO timestamp
156 WHERE lower(domain_name||domain_tail) = lower(name);
162 FROM regano.bailiwicks
163 WHERE lower(domain_tail) = lower(name)
164 OR lower(domain_tail) = '.'||lower(name);
166 SELECT MAX(last_update) INTO timestamp
168 WHERE lower(domain_tail) = lower(name)
169 OR lower(domain_tail) = '.'||lower(name);
175 $$ LANGUAGE plpgsql STABLE STRICT SECURITY DEFINER;
176 ALTER FUNCTION regano_api.domain_last_update (regano.dns_fqdn)
180 -- Create a new user account.
181 CREATE OR REPLACE FUNCTION regano_api.user_register
182 (text, regano.password, text, text)
185 username_ ALIAS FOR $1;
186 password_ ALIAS FOR $2;
187 contact_name ALIAS FOR $3;
188 contact_email ALIAS FOR $4;
190 crypt_alg CONSTANT text NOT NULL
191 := (regano.config_get('auth/crypt')).text;
192 crypt_iter CONSTANT integer NOT NULL
193 := (regano.config_get('auth/crypt')).number;
195 new_user_id bigint; -- row ID of new user record
197 INSERT INTO users (username, password, contact_id)
198 VALUES (username_, ROW(password_.xdigest, password_.xsalt,
199 crypt(password_.digest,
200 gen_salt(crypt_alg, crypt_iter))), 1)
201 RETURNING id INTO STRICT new_user_id;
202 INSERT INTO contacts (owner_id, id, name, email)
203 VALUES (new_user_id, 1, contact_name, contact_email);
205 $$ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER;
206 ALTER FUNCTION regano_api.user_register (text, regano.password, text, text)
209 -- Get the external digest algorithm and salt for a user.
210 CREATE OR REPLACE FUNCTION regano_api.user_get_salt_info (text)
211 RETURNS regano.password AS $$
213 username_ ALIAS FOR $1;
215 password_ regano.password;
217 SELECT (password).xdigest, (password).xsalt INTO password_
218 FROM regano.users WHERE (username = username_);
220 -- return an unspecified record to impede timing attacks
221 SELECT (password).xdigest, (password).xsalt INTO password_
222 FROM regano.users FETCH FIRST 1 ROW ONLY;
227 $$ LANGUAGE plpgsql STABLE STRICT SECURITY DEFINER;
228 ALTER FUNCTION regano_api.user_get_salt_info (text)
231 -- Begin a session for a user.
232 CREATE OR REPLACE FUNCTION regano_api.user_login
233 (text, regano.password)
237 username ALIAS FOR $1;
238 password ALIAS FOR $2;
240 crypt_alg CONSTANT text NOT NULL
241 := (regano.config_get('auth/crypt')).text;
242 crypt_iter CONSTANT integer NOT NULL
243 := (regano.config_get('auth/crypt')).number;
244 max_age CONSTANT interval NOT NULL
245 := (regano.config_get('session/max_age')).interval;
247 user_id bigint; -- row ID of user record
248 stored_pw text; -- password hash from database
249 session_id uuid; -- session ID
251 SELECT id, (regano.users.password).digest INTO user_id, stored_pw
252 FROM regano.users WHERE (regano.users.username = var.username);
253 IF NOT FOUND OR stored_pw = '!' THEN
254 -- fake a stored password to impede timing attacks
255 stored_pw := gen_salt(crypt_alg, crypt_iter);
257 -- clean up expired sessions
258 DELETE FROM regano.sessions WHERE start < (CURRENT_TIMESTAMP - max_age);
259 -- verify password; note that a bare salt cannot match any hash
260 IF crypt(password.digest, stored_pw) = stored_pw THEN
262 INSERT INTO regano.sessions (id, user_id)
263 VALUES (gen_random_uuid(), user_id)
264 RETURNING id INTO STRICT session_id;
271 $$ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER;
272 ALTER FUNCTION regano_api.user_login (text, regano.password)
275 -- Change a logged-in user's password.
276 CREATE OR REPLACE FUNCTION regano_api.user_change_password
277 (uuid, regano.password, regano.password)
278 RETURNS boolean AS $$
280 session_id ALIAS FOR $1;
284 crypt_alg CONSTANT text NOT NULL
285 := (regano.config_get('auth/crypt')).text;
286 crypt_iter CONSTANT integer NOT NULL
287 := (regano.config_get('auth/crypt')).number;
289 user_id bigint; -- row ID of user record
291 SELECT regano.sessions.user_id INTO user_id
292 FROM regano.sessions WHERE id = session_id;
297 new_pw.digest := crypt(new_pw.digest, gen_salt(crypt_alg, crypt_iter));
300 SET password = new_pw
301 WHERE ((id = user_id) AND
302 (crypt(old_pw.digest, (regano.users.password).digest) =
303 (regano.users.password).digest));
306 $$ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER;
307 ALTER FUNCTION regano_api.user_change_password
308 (uuid, regano.password, regano.password)
311 -- Recover a lost password and open a session.
312 CREATE OR REPLACE FUNCTION regano_api.user_recover_password
313 (uuid, uuid, regano.password)
316 verification_id ALIAS FOR $1;
317 verification_key ALIAS FOR $2;
320 crypt_alg CONSTANT text NOT NULL
321 := (regano.config_get('auth/crypt')).text;
322 crypt_iter CONSTANT integer NOT NULL
323 := (regano.config_get('auth/crypt')).number;
325 verification regano.contact_verifications%ROWTYPE;
328 SELECT * INTO verification
329 FROM regano.contact_verifications
330 WHERE type = 'account_recovery'
331 AND id = verification_id AND key = verification_key;
336 new_pw.digest := crypt(new_pw.digest, gen_salt(crypt_alg, crypt_iter));
339 SET password = new_pw
340 WHERE id = verification.user_id;
341 UPDATE regano.contacts
342 SET email_verified = TRUE
343 WHERE owner_id = verification.user_id AND id = verification.contact_id;
345 INSERT INTO regano.sessions (id, user_id)
346 VALUES (gen_random_uuid(), verification.user_id)
347 RETURNING id INTO STRICT session_id;
350 FROM regano.contact_verifications
351 WHERE id = verification_id;
354 $$ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER;
355 ALTER FUNCTION regano_api.user_recover_password
356 (uuid, uuid, regano.password)
360 CREATE OR REPLACE FUNCTION regano_api.session_logout (uuid) RETURNS void AS $$
361 DELETE FROM regano.sessions WHERE id = $1
362 $$ LANGUAGE SQL VOLATILE STRICT SECURITY DEFINER;
363 ALTER FUNCTION regano_api.session_logout (uuid)
366 -- Retrieve username for a session, update session activity timestamp, and
367 -- perform auto-logout if the session has expired.
368 CREATE OR REPLACE FUNCTION regano_api.session_check (uuid)
374 max_age CONSTANT interval NOT NULL
375 := (regano.config_get('session/max_age')).interval;
376 max_idle CONSTANT interval NOT NULL
377 := (regano.config_get('session/max_idle')).interval;
379 session regano.sessions%ROWTYPE;
381 SELECT * INTO session
382 FROM regano.sessions WHERE regano.sessions.id = var.id;
384 IF ((CURRENT_TIMESTAMP - session.activity) > max_idle) OR
385 ((CURRENT_TIMESTAMP - session.start) > max_age) THEN
386 -- session is expired
387 PERFORM regano_api.session_logout(session.id);
389 ELSIF ((CURRENT_TIMESTAMP - session.activity) * 4 > max_idle) THEN
390 -- update activity timestamp
391 UPDATE regano.sessions
392 SET activity = CURRENT_TIMESTAMP
393 WHERE regano.sessions.id = var.id;
396 -- no such session exists
399 RETURN regano.username(session);
401 $$ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER;
402 ALTER FUNCTION regano_api.session_check (uuid)
405 -- Retrieve the current user's user record, sans password.
406 CREATE OR REPLACE FUNCTION regano_api.user_info
408 RETURNS regano.users AS $$
410 user regano.users%ROWTYPE;
412 SELECT * INTO STRICT user
413 FROM regano.users WHERE id = regano.session_user_id(session_id);
414 user.password := NULL;
417 $$ LANGUAGE plpgsql STABLE STRICT SECURITY DEFINER;
418 ALTER FUNCTION regano_api.user_info (uuid)
421 -- Retrieve the ID of the current user's primary contact record.
422 CREATE OR REPLACE FUNCTION regano_api.contact_primary_id
424 RETURNS integer AS $$
425 SELECT contact_id FROM regano.users WHERE id = regano.session_user_id($1)
426 $$ LANGUAGE SQL STABLE STRICT SECURITY DEFINER;
427 ALTER FUNCTION regano_api.contact_primary_id (uuid)
430 -- Change the primary contact for the current user.
431 CREATE OR REPLACE FUNCTION regano_api.user_set_primary_contact
432 (session_id uuid, contact_id integer)
435 domain_term CONSTANT interval NOT NULL
436 := (regano.config_get('domain/term')).interval;
438 contact regano.contacts%ROWTYPE;
439 session regano.sessions%ROWTYPE;
440 pending_domain regano.pending_domains%ROWTYPE;
442 SELECT * INTO STRICT session FROM regano.sessions WHERE id = session_id;
443 SELECT * INTO STRICT contact FROM regano.contacts
444 WHERE owner_id = session.user_id AND id = contact_id;
446 IF NOT contact.email_verified THEN
448 'Only a verified email address may be set as primary contact.';
450 -- update user record
452 SET contact_id = contact.id
453 WHERE id = session.user_id;
454 -- check for a pending domain
455 SELECT * INTO pending_domain
456 FROM regano.pending_domains
457 WHERE pending_domains.user_id = session.user_id;
459 -- register the pending domain
461 FROM regano.pending_domains
462 WHERE domain_name = pending_domain.domain_name
463 AND domain_tail = pending_domain.domain_tail;
464 INSERT INTO regano.domains
465 (domain_name, domain_tail, owner_id, expiration)
466 VALUES (pending_domain.domain_name, pending_domain.domain_tail,
467 session.user_id, now() + domain_term);
470 $$ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER;
471 ALTER FUNCTION regano_api.user_set_primary_contact (uuid, integer)
474 -- Retrieve all contact records belonging to the current user.
475 CREATE OR REPLACE FUNCTION regano_api.contact_list
477 RETURNS SETOF regano.contacts AS $$
478 SELECT * FROM regano.contacts WHERE owner_id = regano.session_user_id($1)
479 $$ LANGUAGE SQL STABLE STRICT SECURITY DEFINER;
480 ALTER FUNCTION regano_api.contact_list (uuid)
483 -- Add a contact record for the current user.
484 CREATE OR REPLACE FUNCTION regano_api.contact_add
485 (session_id uuid, name text, email text)
486 RETURNS integer AS $$
487 INSERT INTO regano.contacts (owner_id, id, name, email)
488 VALUES (regano.session_user_id($1),
489 regano.contact_next_id(regano.session_user_id($1)), $2, $3)
491 $$ LANGUAGE SQL VOLATILE STRICT SECURITY DEFINER;
492 ALTER FUNCTION regano_api.contact_add (uuid, text, text)
495 -- Remove a contact record for the current user.
496 CREATE OR REPLACE FUNCTION regano_api.contact_remove
497 (session_id uuid, contact_id integer)
500 user_id CONSTANT bigint NOT NULL
501 := regano.session_user_id(session_id);
502 renumbering CURSOR (user_id bigint)
505 WHERE owner_id = user_id
512 WHERE owner_id = user_id AND id = contact_id;
513 FOR contact IN renumbering (user_id) LOOP
515 UPDATE regano.contacts
517 WHERE CURRENT OF renumbering;
520 $$ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER;
521 ALTER FUNCTION regano_api.contact_remove (uuid, integer)
524 -- Update the name field of a contact record.
525 CREATE OR REPLACE FUNCTION regano_api.contact_update_name
526 (session_id uuid, contact_id integer, value text)
529 contact regano.contacts%ROWTYPE;
530 session regano.sessions%ROWTYPE;
532 SELECT * INTO STRICT session FROM regano.sessions WHERE id = session_id;
533 SELECT * INTO STRICT contact FROM regano.contacts
534 WHERE owner_id = session.user_id AND id = contact_id
537 UPDATE regano.contacts
539 WHERE owner_id = session.user_id AND id = contact_id;
541 $$ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER;
542 ALTER FUNCTION regano_api.contact_update_name (uuid, integer, text)
545 -- Update the email address field of a contact record.
546 CREATE OR REPLACE FUNCTION regano_api.contact_update_email
547 (session_id uuid, contact_id integer, value text)
550 contact regano.contacts%ROWTYPE;
551 session regano.sessions%ROWTYPE;
553 primary_contact_id integer;
555 SELECT * INTO STRICT session FROM regano.sessions WHERE id = session_id;
556 SELECT users.contact_id INTO STRICT primary_contact_id
557 FROM regano.users WHERE id = session.user_id;
558 SELECT * INTO STRICT contact FROM regano.contacts
559 WHERE owner_id = session.user_id AND id = contact_id
562 IF contact_id = primary_contact_id AND contact.email_verified THEN
564 'Verified email address (%) for primary contact (%) may not be changed.',
565 contact.email, contact_id;
568 -- cancel any in-progress address verification
569 DELETE FROM regano.contact_verifications
570 WHERE contact_verifications.user_id = session.user_id
571 AND contact_verifications.contact_id = contact.id;
572 -- change the stored email address
573 UPDATE regano.contacts
574 SET email_verified = FALSE, email = value
575 WHERE owner_id = session.user_id AND id = contact_id;
577 $$ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER;
578 ALTER FUNCTION regano_api.contact_update_email (uuid, integer, text)
581 -- Update the PGP key field of a contact record
582 CREATE OR REPLACE FUNCTION regano_api.contact_update_pgp_key
583 (session_id uuid, contact_id integer, value text)
586 contact regano.contacts%ROWTYPE;
587 session regano.sessions%ROWTYPE;
589 SELECT * INTO STRICT session FROM regano.sessions WHERE id = session_id;
590 SELECT * INTO STRICT contact FROM regano.contacts
591 WHERE owner_id = session.user_id AND id = contact_id
595 IF char_length(value) > 0 THEN
596 UPDATE regano.contacts
597 SET pgp_key = value, pgp_key_id = pgp_key_id(dearmor(value))
598 WHERE owner_id = session.user_id AND id = contact_id;
599 ELSIF value IS NULL OR char_length(value) = 0 THEN
600 UPDATE regano.contacts
601 SET pgp_key = NULL, pgp_key_id = NULL
602 WHERE owner_id = session.user_id AND id = contact_id;
605 $$ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER;
606 ALTER FUNCTION regano_api.contact_update_pgp_key (uuid, integer, text)
610 -- Begin the process of verifying a contact record.
611 CREATE OR REPLACE FUNCTION regano_api.contact_verify_begin
612 (session_id uuid, contact_id integer)
615 contact regano.contacts%ROWTYPE;
616 session regano.sessions%ROWTYPE;
618 SELECT * INTO STRICT session FROM regano.sessions WHERE id = session_id;
619 SELECT * INTO STRICT contact FROM regano.contacts
620 WHERE owner_id = session.user_id AND id = contact_id;
622 DELETE FROM regano.contact_verifications
623 WHERE contact_verifications.contact_id = contact_verify_begin.contact_id
624 AND contact_verifications.user_id = session.user_id;
625 INSERT INTO regano.contact_verifications (id, key, type, user_id, contact_id)
626 VALUES (gen_random_uuid(), gen_random_uuid(), 'email_address',
627 session.user_id, contact_id);
628 NOTIFY regano__contact_verifications;
630 $$ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER;
631 ALTER FUNCTION regano_api.contact_verify_begin (uuid, integer)
634 -- Complete verification of a contact record.
635 CREATE OR REPLACE FUNCTION regano_api.contact_verify_complete
636 (verification_id uuid, key uuid)
637 RETURNS boolean AS $$
640 domain_term CONSTANT interval NOT NULL
641 := (regano.config_get('domain/term')).interval;
642 max_age CONSTANT interval NOT NULL
643 := (regano.config_get('verify/max_age')).interval;
645 pending_domain regano.pending_domains%ROWTYPE;
646 verification regano.contact_verifications%ROWTYPE;
648 is_primary_contact boolean;
650 -- clean up expired verifications
652 FROM regano.contact_verifications
653 WHERE start < (CURRENT_TIMESTAMP - max_age);
654 -- look up the provided verification ID
655 SELECT * INTO verification
656 FROM regano.contact_verifications
657 WHERE (type = 'email_address') AND (id = verification_id) AND
658 (contact_verifications.key = contact_verify_complete.key);
662 -- mark email address as verified
663 UPDATE regano.contacts
664 SET email_verified = TRUE
665 WHERE owner_id = verification.user_id AND id = verification.contact_id;
666 -- check if a primary contact was verified
667 SELECT users.contact_id = verification.contact_id
668 INTO STRICT is_primary_contact
670 WHERE id = verification.user_id;
671 -- check for a pending domain
672 SELECT * INTO pending_domain
673 FROM regano.pending_domains
674 WHERE pending_domains.user_id = verification.user_id;
675 IF FOUND AND is_primary_contact THEN
676 -- register the pending domain
678 FROM regano.pending_domains
679 WHERE domain_name = pending_domain.domain_name
680 AND domain_tail = pending_domain.domain_tail;
681 INSERT INTO regano.domains
682 (domain_name, domain_tail, owner_id, expiration)
683 VALUES (pending_domain.domain_name, pending_domain.domain_tail,
684 verification.user_id, now() + domain_term);
686 -- clean up the successful verification
688 FROM regano.contact_verifications
689 WHERE id = verification_id;
692 $$ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER;
693 ALTER FUNCTION regano_api.contact_verify_complete (uuid, uuid)
697 -- Retrieve information about a pending domain belonging to the current user.
698 CREATE OR REPLACE FUNCTION regano_api.domain_check_pending
700 RETURNS regano.pending_domain AS $$
701 SELECT domain_name||domain_tail AS name,
702 start, start + regano.config_get('domain/pend_term') AS expire
703 FROM regano.pending_domains WHERE user_id = regano.session_user_id($1)
704 $$ LANGUAGE SQL STABLE STRICT SECURITY DEFINER;
705 ALTER FUNCTION regano_api.domain_check_pending (uuid)
708 -- Retrieve all domains belonging to the current user.
709 -- The domain table is public; this is for the account overview page.
710 CREATE OR REPLACE FUNCTION regano_api.domain_list
712 RETURNS SETOF regano.domain AS $$
713 SELECT domain_name||domain_tail AS name, registered, expiration, last_update,
714 CASE WHEN now() < expiration
715 THEN 'REGISTERED'::regano.domain_status
716 ELSE 'EXPIRED'::regano.domain_status
718 FROM regano.domains WHERE owner_id = regano.session_user_id($1)
719 $$ LANGUAGE SQL STABLE STRICT SECURITY DEFINER;
720 ALTER FUNCTION regano_api.domain_list (uuid)
723 -- Register an available domain.
724 CREATE OR REPLACE FUNCTION regano_api.domain_register
725 (uuid, regano.dns_fqdn)
726 RETURNS regano.domain_status AS $$
729 session_id ALIAS FOR $1;
732 domain_term CONSTANT interval NOT NULL
733 := (regano.config_get('domain/term')).interval;
735 user_id CONSTANT bigint NOT NULL
736 := regano.session_user_id(session_id);
738 verified boolean; -- verified email address on file?
740 primary_label regano.dns_label;
741 tail regano.dns_fqdn;
743 primary_label := substring(name from '^([^.]+)[.]');
744 tail := substring(name from '^[^.]+([.].+[.])$');
746 IF regano_api.domain_status(name) <> 'AVAILABLE' THEN
747 RETURN regano_api.domain_status(name);
750 SELECT email_verified INTO STRICT verified
751 FROM regano.users JOIN regano.contacts
752 ON owner_id = user_id AND contact_id = contacts.id
753 WHERE regano.users.id = user_id;
756 -- user has a verified email address; register the domain now
757 INSERT INTO regano.domains
758 (domain_name, domain_tail, owner_id, expiration)
759 VALUES (primary_label, tail, user_id, now() + domain_term);
762 -- no verified email address on file; registration will be pending
763 INSERT INTO regano.pending_domains
764 (domain_name, domain_tail, user_id)
765 VALUES (primary_label, tail, user_id);
769 $$ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER;
770 ALTER FUNCTION regano_api.domain_register (uuid, regano.dns_fqdn)
774 CREATE OR REPLACE FUNCTION regano_api.domain_renew
775 (uuid, regano.dns_fqdn)
776 RETURNS timestamp with time zone AS $$
778 session_id ALIAS FOR $1;
781 domain_term CONSTANT interval NOT NULL
782 := (regano.config_get('domain/term')).interval;
784 user_id CONSTANT bigint NOT NULL
785 := regano.session_user_id(session_id);
787 primary_label regano.dns_label;
788 tail regano.dns_fqdn;
790 domain regano.domains%ROWTYPE;
791 result timestamp with time zone;
793 primary_label := substring(name from '^([^.]+)[.]');
794 tail := substring(name from '^[^.]+([.].+[.])$');
796 SELECT * INTO STRICT domain
798 WHERE (lower(primary_label) = lower(domain_name))
799 AND (lower(tail) = lower(domain_tail))
802 IF user_id <> domain.owner_id THEN
804 'attempt made to renew domain (%) not belonging to current user (%)',
805 name, regano.username(session_id);
808 UPDATE regano.domains
809 SET expiration = now() + domain_term,
812 RETURNING expiration INTO STRICT result;
815 $$ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER;
816 ALTER FUNCTION regano_api.domain_renew (uuid, regano.dns_fqdn)
819 -- Immediately expire a domain
820 CREATE OR REPLACE FUNCTION regano_api.domain_release
821 (uuid, regano.dns_fqdn)
824 session_id ALIAS FOR $1;
827 user_id CONSTANT bigint NOT NULL
828 := regano.session_user_id(session_id);
830 primary_label regano.dns_label;
831 tail regano.dns_fqdn;
833 domain regano.domains%ROWTYPE;
835 primary_label := substring(name from '^([^.]+)[.]');
836 tail := substring(name from '^[^.]+([.].+[.])$');
839 FROM regano.reserved_domains
840 WHERE (domain_name = lower(primary_label));
845 SELECT * INTO STRICT domain
847 WHERE (lower(primary_label) = lower(domain_name))
848 AND (lower(tail) = lower(domain_tail))
851 IF user_id <> domain.owner_id THEN
853 'attempt made to release domain (%) not belonging to current user (%)',
854 name, regano.username(session_id);
857 UPDATE regano.domains
858 SET expiration = now(),
860 WHERE id = domain.id;
862 $$ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER;
863 ALTER FUNCTION regano_api.domain_release (uuid, regano.dns_fqdn)
866 -- Set default TTL for records in a domain
867 CREATE OR REPLACE FUNCTION regano_api.domain_set_default_ttl
868 (uuid, regano.dns_fqdn, regano.dns_interval)
871 session_id ALIAS FOR $1;
873 new_ttl ALIAS FOR $3;
875 user_id CONSTANT bigint NOT NULL
876 := regano.session_user_id(session_id);
878 primary_label regano.dns_label;
879 tail regano.dns_fqdn;
881 domain regano.domains%ROWTYPE;
883 primary_label := substring(name from '^([^.]+)[.]');
884 tail := substring(name from '^[^.]+([.].+[.])$');
886 SELECT * INTO STRICT domain
888 WHERE (lower(primary_label) = lower(domain_name))
889 AND (lower(tail) = lower(domain_tail))
892 IF user_id <> domain.owner_id THEN
894 'attempt made to set TTL for domain (%) not belonging to current user (%)',
895 name, regano.username(session_id);
898 UPDATE regano.domains
899 SET default_ttl = new_ttl,
901 WHERE id = domain.id;
903 $$ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER;
904 ALTER FUNCTION regano_api.domain_set_default_ttl
905 (uuid, regano.dns_fqdn, regano.dns_interval)
908 -- Updating domain records is done in multiple steps, all in a single
909 -- database transaction. First, the existing records for the domain are
910 -- removed. Second, new records are inserted in order. Third, the
911 -- database transaction is committed.
913 -- Remove existing records for a domain
914 CREATE OR REPLACE FUNCTION regano_api.zone_clear
916 zone_name regano.dns_fqdn)
919 domain regano.domains%ROWTYPE;
921 domain := regano.zone_verify_access(session_id, zone_name, 'clear zone');
924 FROM regano.domain_records
925 WHERE domain_id = domain.id;
926 UPDATE regano.domains
927 SET last_update = now()
928 WHERE id = domain.id;
929 NOTIFY regano__domain_records;
931 $$ LANGUAGE plpgsql VOLATILE STRICT SECURITY DEFINER;
932 ALTER FUNCTION regano_api.zone_clear (uuid, regano.dns_fqdn)
935 -- Add an SOA record for a domain
936 -- NOTE: A domain may only have one SOA record, at the domain root, with
937 -- sequence number zero.
938 CREATE OR REPLACE FUNCTION regano_api.zone_add_SOA
940 zone_name regano.dns_fqdn,
941 rec_ttl regano.dns_interval,
942 SOA_master regano.dns_fqdn,
943 SOA_mbox regano.dns_email,
944 SOA_refresh regano.dns_interval,
945 SOA_retry regano.dns_interval,
946 SOA_expire regano.dns_interval,
947 SOA_minimum regano.dns_interval)
950 domain regano.domains%ROWTYPE;
952 domain := regano.zone_verify_access(session_id, zone_name, 'add SOA');
954 INSERT INTO regano.domain_records
955 (domain_id, seq_no, type, name, ttl, data_RR_SOA)
956 VALUES (domain.id, 0, 'SOA', '@', rec_ttl,
957 ROW(SOA_master, SOA_mbox, SOA_refresh, SOA_retry,
958 SOA_expire, SOA_minimum));
959 UPDATE regano.domains
960 SET last_update = now()
961 WHERE id = domain.id;
963 $$ LANGUAGE plpgsql VOLATILE CALLED ON NULL INPUT SECURITY DEFINER;
964 ALTER FUNCTION regano_api.zone_add_SOA
965 (uuid, regano.dns_fqdn, regano.dns_interval, regano.dns_fqdn,
966 regano.dns_email, regano.dns_interval, regano.dns_interval,
967 regano.dns_interval, regano.dns_interval)
970 -- Add a record that stores another DNS name
971 CREATE OR REPLACE FUNCTION regano_api.zone_add_name
973 zone_name regano.dns_fqdn,
974 rec_ttl regano.dns_interval,
975 rec_name regano.dns_name,
976 rec_type regano.dns_record_type,
977 rec_data regano.dns_name)
980 domain regano.domains%ROWTYPE;
982 rec_name_c CONSTANT regano.dns_name NOT NULL
983 := regano.canonicalize_record_name(rec_name,
986 domain := regano.zone_verify_access(session_id, zone_name, 'add name');
987 new_seq_no := regano.zone_next_seq_no(domain.id);
989 INSERT INTO regano.domain_records
990 (domain_id, seq_no, type, name, ttl, data_name)
991 VALUES (domain.id, new_seq_no, rec_type, rec_name_c, rec_ttl, rec_data);
992 UPDATE regano.domains
993 SET last_update = now()
994 WHERE id = domain.id;
996 $$ LANGUAGE plpgsql VOLATILE CALLED ON NULL INPUT SECURITY DEFINER;
997 ALTER FUNCTION regano_api.zone_add_name
998 (uuid, regano.dns_fqdn, regano.dns_interval, regano.dns_name,
999 regano.dns_record_type, regano.dns_name)
1002 -- Add a record that stores free-form text
1003 CREATE OR REPLACE FUNCTION regano_api.zone_add_text
1005 zone_name regano.dns_fqdn,
1006 rec_ttl regano.dns_interval,
1007 rec_name regano.dns_name,
1008 rec_type regano.dns_record_type,
1012 domain regano.domains%ROWTYPE;
1014 rec_name_c CONSTANT regano.dns_name NOT NULL
1015 := regano.canonicalize_record_name(rec_name,
1018 domain := regano.zone_verify_access(session_id, zone_name, 'add text');
1019 new_seq_no := regano.zone_next_seq_no(domain.id);
1021 INSERT INTO regano.domain_records
1022 (domain_id, seq_no, type, name, ttl, data_text)
1023 VALUES (domain.id, new_seq_no, rec_type, rec_name_c, rec_ttl, rec_data);
1024 UPDATE regano.domains
1025 SET last_update = now()
1026 WHERE id = domain.id;
1028 $$ LANGUAGE plpgsql VOLATILE CALLED ON NULL INPUT SECURITY DEFINER;
1029 ALTER FUNCTION regano_api.zone_add_text
1030 (uuid, regano.dns_fqdn, regano.dns_interval, regano.dns_name,
1031 regano.dns_record_type, text)
1035 CREATE OR REPLACE FUNCTION regano_api.zone_add_A
1037 zone_name regano.dns_fqdn,
1038 rec_ttl regano.dns_interval,
1039 rec_name regano.dns_name,
1040 rec_data regano.dns_RR_A)
1043 domain regano.domains%ROWTYPE;
1045 rec_name_c CONSTANT regano.dns_name NOT NULL
1046 := regano.canonicalize_record_name(rec_name,
1049 domain := regano.zone_verify_access(session_id, zone_name, 'add A');
1050 new_seq_no := regano.zone_next_seq_no(domain.id);
1052 INSERT INTO regano.domain_records
1053 (domain_id, seq_no, type, name, ttl, data_RR_A)
1054 VALUES (domain.id, new_seq_no, 'A', rec_name_c, rec_ttl, rec_data);
1055 UPDATE regano.domains
1056 SET last_update = now()
1057 WHERE id = domain.id;
1059 $$ LANGUAGE plpgsql VOLATILE CALLED ON NULL INPUT SECURITY DEFINER;
1060 ALTER FUNCTION regano_api.zone_add_A
1061 (uuid, regano.dns_fqdn, regano.dns_interval, regano.dns_name,
1065 -- Add an AAAA record
1066 CREATE OR REPLACE FUNCTION regano_api.zone_add_AAAA
1068 zone_name regano.dns_fqdn,
1069 rec_ttl regano.dns_interval,
1070 rec_name regano.dns_name,
1071 rec_data regano.dns_RR_AAAA)
1074 domain regano.domains%ROWTYPE;
1076 rec_name_c CONSTANT regano.dns_name NOT NULL
1077 := regano.canonicalize_record_name(rec_name,
1080 domain := regano.zone_verify_access(session_id, zone_name, 'add AAAA');
1081 new_seq_no := regano.zone_next_seq_no(domain.id);
1083 INSERT INTO regano.domain_records
1084 (domain_id, seq_no, type, name, ttl, data_RR_AAAA)
1085 VALUES (domain.id, new_seq_no, 'AAAA', rec_name_c, rec_ttl, rec_data);
1086 UPDATE regano.domains
1087 SET last_update = now()
1088 WHERE id = domain.id;
1090 $$ LANGUAGE plpgsql VOLATILE CALLED ON NULL INPUT SECURITY DEFINER;
1091 ALTER FUNCTION regano_api.zone_add_AAAA
1092 (uuid, regano.dns_fqdn, regano.dns_interval, regano.dns_name,
1097 CREATE OR REPLACE FUNCTION regano_api.zone_add_DS
1099 zone_name regano.dns_fqdn,
1100 rec_ttl regano.dns_interval,
1101 rec_name regano.dns_name,
1102 DS_key_tag regano.uint16bit,
1103 DS_algorithm regano.uint8bit,
1104 DS_digest_type regano.uint8bit,
1105 DS_digest regano.hexstring)
1108 domain regano.domains%ROWTYPE;
1110 rec_name_c CONSTANT regano.dns_name NOT NULL
1111 := regano.canonicalize_record_name(rec_name,
1114 domain := regano.zone_verify_access(session_id, zone_name, 'add DS');
1115 new_seq_no := regano.zone_next_seq_no(domain.id);
1117 INSERT INTO regano.domain_records
1118 (domain_id, seq_no, type, name, ttl, data_RR_DS)
1119 VALUES (domain.id, new_seq_no, 'DS', rec_name_c, rec_ttl,
1120 ROW(DS_key_tag, DS_algorithm, DS_digest_type, DS_digest));
1121 UPDATE regano.domains
1122 SET last_update = now()
1123 WHERE id = domain.id;
1125 $$ LANGUAGE plpgsql VOLATILE CALLED ON NULL INPUT SECURITY DEFINER;
1126 ALTER FUNCTION regano_api.zone_add_DS
1127 (uuid, regano.dns_fqdn, regano.dns_interval, regano.dns_name,
1128 regano.uint16bit, regano.uint8bit, regano.uint8bit,
1133 CREATE OR REPLACE FUNCTION regano_api.zone_add_MX
1135 zone_name regano.dns_fqdn,
1136 rec_ttl regano.dns_interval,
1137 rec_name regano.dns_name,
1138 MX_preference regano.uint16bit,
1139 MX_exchange regano.dns_name)
1142 domain regano.domains%ROWTYPE;
1144 rec_name_c CONSTANT regano.dns_name NOT NULL
1145 := regano.canonicalize_record_name(rec_name,
1148 domain := regano.zone_verify_access(session_id, zone_name, 'add MX');
1149 new_seq_no := regano.zone_next_seq_no(domain.id);
1151 INSERT INTO regano.domain_records
1152 (domain_id, seq_no, type, name, ttl, data_RR_MX)
1153 VALUES (domain.id, new_seq_no, 'MX', rec_name_c, rec_ttl,
1154 ROW(MX_preference, MX_exchange));
1155 UPDATE regano.domains
1156 SET last_update = now()
1157 WHERE id = domain.id;
1159 $$ LANGUAGE plpgsql VOLATILE CALLED ON NULL INPUT SECURITY DEFINER;
1160 ALTER FUNCTION regano_api.zone_add_MX
1161 (uuid, regano.dns_fqdn, regano.dns_interval, regano.dns_name,
1162 regano.uint16bit, regano.dns_name)
1166 CREATE OR REPLACE FUNCTION regano_api.zone_add_SRV
1168 zone_name regano.dns_fqdn,
1169 rec_ttl regano.dns_interval,
1170 rec_name regano.dns_name,
1171 SRV_priority regano.uint16bit,
1172 SRV_weight regano.uint16bit,
1173 SRV_port regano.uint16bit,
1174 SRV_target regano.dns_fqdn)
1177 domain regano.domains%ROWTYPE;
1179 rec_name_c CONSTANT regano.dns_name NOT NULL
1180 := regano.canonicalize_record_name(rec_name,
1183 domain := regano.zone_verify_access(session_id, zone_name, 'add SRV');
1184 new_seq_no := regano.zone_next_seq_no(domain.id);
1186 INSERT INTO regano.domain_records
1187 (domain_id, seq_no, type, name, ttl, data_RR_SRV)
1188 VALUES (domain.id, new_seq_no, 'SRV', rec_name_c, rec_ttl,
1189 ROW(SRV_priority, SRV_weight, SRV_port, SRV_target));
1190 UPDATE regano.domains
1191 SET last_update = now()
1192 WHERE id = domain.id;
1194 $$ LANGUAGE plpgsql VOLATILE CALLED ON NULL INPUT SECURITY DEFINER;
1195 ALTER FUNCTION regano_api.zone_add_SRV
1196 (uuid, regano.dns_fqdn, regano.dns_interval, regano.dns_name,
1197 regano.uint16bit, regano.uint16bit, regano.uint16bit,