Skip to content

Instantly share code, notes, and snippets.

@prestonmcgowan
Last active October 30, 2019 19:32
Show Gist options
  • Save prestonmcgowan/820cda5dcd3d20f4e5ff38042cee1ead to your computer and use it in GitHub Desktop.
Save prestonmcgowan/820cda5dcd3d20f4e5ff38042cee1ead to your computer and use it in GitHub Desktop.
MarkLogic Password Plugin for complexity and history
(: Password rules
:
: at least 12 characters
: has not been used in the previous 24 passwords
: has not been changed in the last day
: does not contain your account or full name
: contains at least three of the the four character groups
: - lower case
: - upper case
: - numbers
: - non-alphas
:)
xquery version "1.0-ml";
declare namespace sec = "http://marklogic.com/xdmp/security";
declare namespace pwd = "http://marklogic.com/extension/plugin/password-check";
declare namespace xev = "http://marklogic.com/xquery-variable";
import module namespace plugin = "http://marklogic.com/extension/plugin" at "/MarkLogic/plugin/plugin.xqy";
declare default function namespace "http://www.w3.org/2005/xpath-functions";
(:: Configuration items ::)
declare variable $username-prefix := "username-dmz-";
declare variable $min-password-length := 12;
declare variable $min-character-sets := 3; (: Total of 4: lowercase, uppercase, numbers, non-alphanumerics :)
declare variable $max-passwords-saved := 10;
declare variable $min-password-change-time := xs:dayTimeDuration("PT24H");
declare variable $error-message :=
"Your password must be at least 12 characters, must not have been used in the previous 24 passwords, and cannot have been changed within the last 24 hours.
Your password must not contain your account or full name. The password must contain three of the four character groups: Lower case, Upper case, numbers, and Non-Alphanumerics"
;
(:: End Configuration Items ::)
declare function pwd:rules(
$old-password as xs:string,
$password as xs:string,
$user as element(sec:user))
(: as xs:string? :)
{
(: Build values that will allow us to ensure the number of character sets needed matches or exceeds $min-character-sets :)
let $lower := if (fn:matches($password, "[a-z]")) then 1 else 0
let $upper := if (fn:matches($password, "[A-Z]")) then 1 else 0
let $numbers := if (fn:matches($password, "[0-9]")) then 1 else 0
let $others := if (fn:matches($password, "[^a-zA-Z0-9].")) then 1 else 0
(: Crypt the password for comparison with old passwords :)
let $crypt := xdmp:crypt($password, $user/fn:data(sec:user-name))
(: Find the last time the password was changed :)
let $last-password-change-dateTime :=
xdmp:parse-dateTime("[Y0001]-[M01]-[D01]T[H01]:[m01]:[s01].[f1][Z]",
(
for $op in $user/sec:old-passwords/sec:password
order by $op/@dateTime descending
return $op
)[1]/fn:data(@dateTime)
)
return
(: Make sure the minimum password length and character sets are used :)
if (fn:string-length($password) lt $min-password-length or ($lower + $upper + $numbers + $others) lt $min-character-sets) then
(xdmp:log("Password Change Failure: 1", "debug"), $error-message)
(: Ensure the password has not been used within the $max-passwords-saved :)
else if (fn:exists($user/sec:old-passwords/sec:password[. eq $crypt])) then
(xdmp:log("Password Change Failure: 2", "debug"), $error-message)
(: Ensure the password does not contain the username or account name :)
else if (
fn:string-length($user/fn:data(sec:user-name)) gt 0
and
fn:contains(
fn:lower-case($password),
fn:replace(fn:lower-case($user/fn:data(sec:user-name)), $username-prefix, "")
)
) then
(xdmp:log("Password Change Failure: 3", "debug"), $error-message)
(: Ensure the last password saved was greater than $min-password-change-time :)
else if ((fn:current-dateTime() - $last-password-change-dateTime) lt $min-password-change-time) then
(xdmp:log("Password Change Failure: 4", "debug"), $error-message)
(: Password is OK, record it in the old password list, and allow the password to be set :)
else
(: Add the password to the used password list, subtract one from the $max-passwords-saved value since we will be adding one :)
if ($user/sec:old-passwords) then
if (fn:count($user/sec:old-passwords/sec:password) ge $max-passwords-saved - 1) then (
xdmp:log("Password Change Success: 1", "debug"),
(: Remove all the old passwords greater than $max-passwords-saved :)
let $trash :=
(
for $op in $user/sec:old-passwords/sec:password
order by $op/@dateTime descending
return $op
)[$max-passwords-saved to fn:last()]
for $p in $trash
return xdmp:node-delete($p)
,
xdmp:node-insert-child($user/sec:old-passwords,
element sec:password {
attribute dateTime { fn:current-dateTime() },
$crypt
}
)
)
else
xdmp:node-insert-child($user/sec:old-passwords,
element sec:password {
attribute dateTime { fn:current-dateTime() },
xdmp:crypt($password, $user/fn:data(sec:user-name))
}
)
else (
xdmp:log("Password Change Success: 2", "debug"),
try {
xdmp:node-insert-child($user,
element sec:old-passwords {
element sec:password {
attribute dateTime { fn:current-dateTime() },
xdmp:crypt($password, $user/fn:data(sec:user-name))
}
}
)
} catch($e) {
() (: Unable to write the first password to the old password list :)
}
)
};
(: Query Console Code :)
(:
let $old-password := "Qwerty123456"
let $new-password := fn:format-dateTime(fn:current-dateTime(), "[MNn][Y0001][M01][D01][h01][m01][s01]")
let $user := /sec:user[sec:user-name eq "test-user"]
return pwd:rules($old-password, $new-password, $user)
:)
(: Uncomment below for MarkLogic Password registration :)
let $map := map:map(),
$_ := map:put($map, "http://marklogic.com/xdmp/security/password-check",
xdmp:function(xs:QName("pwd:rules")))
return
plugin:register($map, "password-check-rules.xqy")
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment