Skip to content

Commit 3e1ff28

Browse files
committed
new builtin function sqlpage.random_string
1 parent af3f128 commit 3e1ff28

File tree

6 files changed

+88
-39
lines changed

6 files changed

+88
-39
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ markdown = { version = "1.0.0-alpha.9", features = ["log"] }
4343
password-hash = "0.5.0"
4444
argon2 = "0.5.0"
4545
actix-web-httpauth = "0.8.0"
46+
rand = "0.8.5"
4647

4748
[build-dependencies]
4849
ureq = "2.6.2"

examples/official-site/sqlpage/migrations/08_functions.sql

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,4 +144,39 @@ VALUES (
144144
'password',
145145
'The password to hash.',
146146
'TEXT'
147-
);
147+
);
148+
149+
INSERT INTO sqlpage_functions ("name", "introduced_in_version", "icon", "description_md")
150+
VALUES (
151+
'random_string',
152+
'0.7.2',
153+
'arrows-shuffle',
154+
'Returns a cryptographically secure random string of the given length.
155+
156+
### Example
157+
158+
Generate a random string of 32 characters and use it as a session ID stored in a cookie:
159+
160+
```sql
161+
INSERT INTO login_session (id, username) VALUES (sqlpage.random_string(32), :username)
162+
RETURNING
163+
''cookie'' AS component,
164+
''session_id'' AS name,
165+
sqlpage.random_string(32) AS value;
166+
```
167+
'
168+
);
169+
INSERT INTO sqlpage_function_parameters (
170+
"function",
171+
"index",
172+
"name",
173+
"description_md",
174+
"type"
175+
)
176+
VALUES (
177+
'random_string',
178+
1,
179+
'length',
180+
'The length of the string to generate.',
181+
'INTEGER'
182+
);

examples/user-authentication/sqlpage/migrations/0000_init.sql

Lines changed: 1 addition & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,8 @@ CREATE TABLE user_info (
33
password_hash TEXT NOT NULL
44
);
55

6-
-- Activate the pgcrypto extension to be able to hash passwords, and generate session IDs.
7-
CREATE EXTENSION IF NOT EXISTS pgcrypto;
8-
96
CREATE TABLE login_session (
107
id TEXT PRIMARY KEY DEFAULT encode(gen_random_bytes(128), 'hex'),
118
username TEXT NOT NULL REFERENCES user_info(username),
129
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
13-
);
14-
15-
16-
-- Returns true if the session is valid, false otherwise
17-
CREATE FUNCTION is_valid_session(user_session text) RETURNS boolean AS $$
18-
BEGIN
19-
RETURN EXISTS(SELECT 1 FROM login_session WHERE id=user_session);
20-
END;
21-
$$ LANGUAGE plpgsql;
22-
23-
-- Takes a session id, does nothing if it is valid, throws an error otherwise.
24-
CREATE FUNCTION raise_error(error_message_text text) RETURNS void AS $$
25-
BEGIN
26-
RAISE EXCEPTION '%', error_message_text;
27-
END;
28-
$$ LANGUAGE plpgsql;
10+
);

src/webserver/database/mod.rs

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -181,16 +181,21 @@ fn extract_req_param<'a>(
181181
StmtParam::BasicAuthUsername => extract_basic_auth_username(request)
182182
.map(Cow::Borrowed)
183183
.map(Some)?,
184-
StmtParam::HashPassword(inner) => {
185-
if let Some(password) = extract_req_param(inner, request)? {
186-
Some(Cow::Owned(hash_password(&password)?))
187-
} else {
188-
None
189-
}
190-
}
184+
StmtParam::HashPassword(inner) => extract_req_param(inner, request)?
185+
.map_or(Ok(None), |x| hash_password(&x).map(Cow::Owned).map(Some))?,
186+
StmtParam::RandomString(len) => Some(Cow::Owned(random_string(*len))),
191187
})
192188
}
193189

190+
fn random_string(len: usize) -> String {
191+
use rand::{distributions::Alphanumeric, Rng};
192+
password_hash::rand_core::OsRng
193+
.sample_iter(&Alphanumeric)
194+
.take(len)
195+
.map(char::from)
196+
.collect()
197+
}
198+
194199
fn hash_password(password: &str) -> anyhow::Result<String> {
195200
let phf = argon2::Argon2::default();
196201
let salt = password_hash::SaltString::generate(&mut password_hash::rand_core::OsRng);
@@ -375,6 +380,7 @@ enum StmtParam {
375380
BasicAuthPassword,
376381
BasicAuthUsername,
377382
HashPassword(Box<StmtParam>),
383+
RandomString(usize),
378384
}
379385

380386
#[actix_web::test]

src/webserver/database/sql.rs

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,8 @@ fn func_call_to_param(func_name: &str, arguments: &mut [FunctionArg]) -> StmtPar
166166
"hash_password" => extract_variable_argument("hash_password", arguments)
167167
.map(Box::new)
168168
.map_or_else(StmtParam::Error, StmtParam::HashPassword),
169+
"random_string" => extract_integer("random_string", arguments)
170+
.map_or_else(StmtParam::Error, StmtParam::RandomString),
169171
unknown_name => {
170172
StmtParam::Error(format!("Unknown function {unknown_name}({arguments:#?})"))
171173
}
@@ -181,16 +183,42 @@ fn extract_single_quoted_string(
181183
Ok(std::mem::take(param_value))
182184
}
183185
_ => Err(format!(
184-
"{func_name}({args}) is not a valid call. Expected a literal single quoted string.",
185-
args = arguments
186-
.iter()
187-
.map(ToString::to_string)
188-
.collect::<Vec<_>>()
189-
.join(", ")
186+
"{func_name}({}) is not a valid call. Expected a literal single quoted string.",
187+
FormatArguments(arguments)
190188
)),
191189
}
192190
}
193191

192+
fn extract_integer(
193+
func_name: &'static str,
194+
arguments: &mut [FunctionArg],
195+
) -> Result<usize, String> {
196+
match arguments.first_mut().and_then(function_arg_expr) {
197+
Some(Expr::Value(Value::Number(param_value, _b))) => param_value
198+
.parse::<usize>()
199+
.map_err(|e| format!("{func_name}({param_value}) failed: {e}")),
200+
_ => Err(format!(
201+
"{func_name}({}) is not a valid call. Expected a literal integer",
202+
FormatArguments(arguments)
203+
)),
204+
}
205+
}
206+
207+
/** This is a helper struct to format a list of arguments for an error message. */
208+
struct FormatArguments<'a>(&'a [FunctionArg]);
209+
impl std::fmt::Display for FormatArguments<'_> {
210+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211+
let mut args = self.0.iter();
212+
if let Some(arg) = args.next() {
213+
write!(f, "{arg}")?;
214+
}
215+
for arg in args {
216+
write!(f, ", {arg}")?;
217+
}
218+
Ok(())
219+
}
220+
}
221+
194222
fn extract_variable_argument(
195223
func_name: &'static str,
196224
arguments: &mut [FunctionArg],
@@ -208,12 +236,8 @@ fn extract_variable_argument(
208236
args.as_mut_slice(),
209237
)),
210238
_ => Err(format!(
211-
"{func_name}({args}) is not a valid call. Expected either a placeholder or a sqlpage function call as argument.",
212-
args = arguments
213-
.iter()
214-
.map(ToString::to_string)
215-
.collect::<Vec<_>>()
216-
.join(", ")
239+
"{func_name}({}) is not a valid call. Expected either a placeholder or a sqlpage function call as argument.",
240+
FormatArguments(arguments)
217241
)),
218242
}
219243
}

0 commit comments

Comments
 (0)