Approach: Group-based branching in queries.conf
Instead of pulling a raw SQL-User-Group attribute directly, the cleanest method is to have the authorize query set control:Group (or Fall-Through into a check) based on the expiration date comparison done in SQL itself. That way radgroupreply handles the actual attribute mapping and you never touch queries.conf again when adding new groups.
1. Modify the group membership query
In queries.conf, find authorize_group_check_query and authorize_group_reply_query. Before that, you need the query that assigns which group a user belongs to. This is usually not a static usergroup table entry; it should be dynamic based on expiry.
Replace your group_membership_query with something like this:
sql
group_membership_query = "\
SELECT groupname \
FROM ( \
SELECT 'expired-profile' AS groupname \
WHERE EXISTS ( \
SELECT 1 FROM radcheck \
WHERE username = '%{SQL-User-Name}' \
AND attribute = 'Expiration' \
AND TO_TIMESTAMP(value, 'DD Mon YYYY HH24:MI:SS') < NOW() \
) \
UNION \
SELECT 'active-profile' AS groupname \
WHERE NOT EXISTS ( \
SELECT 1 FROM radcheck \
WHERE username = '%{SQL-User-Name}' \
AND attribute = 'Expiration' \
AND TO_TIMESTAMP(value, 'DD Mon YYYY HH24:MI:SS') < NOW() \
) \
) AS grp"
Adjust the date format string to match however you store Expiration in radcheck. If you store expiry in a separate subscribers table instead of radcheck, point the subquery there and join on username.
2. Populate radgroupreply with the two groups
sql
INSERT INTO radgroupreply (groupname, attribute, op, value) VALUES
('active-profile', 'Mikrotik-Group', ':=', 'default'),
('active-profile', 'Framed-Pool', ':=', 'main-pool'),
('expired-profile', 'Mikrotik-Group', ':=', 'expired'),
('expired-profile', 'Framed-Pool', ':=', 'expired-pool');
3. Enable group queries in sql module
In mods-enabled/sql, confirm these are uncommented:
read_groups = yes
And in sites-enabled/default, inside authorize, make sure sql is called before expiration and that group reply is not being skipped.
Why this is better than a raw SQL-User-Group variable
Pulling SQL-User-Group as a loose attribute means you would need unlang conditionals in post-auth referencing it directly, and every new profile means editing sites-enabled/default. With group-based branching, adding a third tier (grace period, suspended, etc.) is just one more INSERT into radgroupreply, no config file changes.
If your Expiration values are not stored in radcheck and instead live in a custom billing table, tell me that table's schema and I will adjust the subquery to join against it directly instead of assuming radcheck.