Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce System::User and System::Group #7725

Merged
merged 47 commits into from Jul 27, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
ce912de
src: System::User and System::Group
woodruffw Apr 29, 2019
e05e20e
src: Document System::User and System::Group
woodruffw Apr 29, 2019
1774301
Update src/system/user.cr
Sija Apr 30, 2019
66989d2
Update src/crystal/system/unix/user.cr
Sija Apr 30, 2019
f23218f
spec: Add specs for System::User and System::Group
woodruffw May 1, 2019
274907e
src: Exception, self nits
woodruffw May 1, 2019
1eeda59
spec: Fix exception message match
woodruffw May 1, 2019
1b63e44
src: Update passwd structure for Darwin
woodruffw May 1, 2019
edda4e6
src: Update passwd structure for OpenBSD
woodruffw May 1, 2019
6ba7c3b
spec: Test a different group on macOS
woodruffw May 1, 2019
dacbf7c
spec: OpenBSD also uses the wheel group
woodruffw May 1, 2019
b6c11f1
spec: Invert feature test
woodruffw May 1, 2019
674d39b
spec: Add typechecks for Group/User members
woodruffw May 1, 2019
dfd55ae
src: Remove private getters on User and Group
woodruffw May 4, 2019
541a3a9
spec: Use `id` to get user/group names and IDs
woodruffw May 4, 2019
29fb389
spec: Additional user/group name/id checks
woodruffw May 4, 2019
968c278
spec: Try `whoami` instead of `id -un`
woodruffw May 5, 2019
ccaefd8
Revert "spec: Try `whoami` instead of `id -un`"
woodruffw May 5, 2019
afd3e7e
ci: Try `id -un`
woodruffw May 5, 2019
9f6922b
Revert "ci: Try `id -un`"
woodruffw May 5, 2019
e88ad10
ci: Mount passwd and group files
woodruffw May 5, 2019
3f74d2d
src, spec: NotFound -> NotFoundError
woodruffw May 6, 2019
0b186dd
src, spec: Remove Group#password, User#password
woodruffw May 6, 2019
7e3dc5b
spec: Remove pointless type checks
woodruffw May 6, 2019
82f9af9
src, spec: Flatten hierarchy
woodruffw May 7, 2019
adbaefc
src: Remove old comment
woodruffw Jun 10, 2019
fa39723
spec, src: Add full names for System::Users
woodruffw Jun 10, 2019
16a4639
user: Extract user from gecos before constructor
woodruffw Jun 12, 2019
1a1d8ea
src, spec: Represent user and group IDs as Strings
woodruffw Jun 12, 2019
3dc6e18
src: Cap buffers, use stack buffer
woodruffw Jul 8, 2019
3ab2c93
src: More initial stack-allocated buffers
woodruffw Jul 8, 2019
653fecf
src: Use to_u32 directly
woodruffw Jul 21, 2019
51cf041
src: ActiveRecord-style API
woodruffw Jul 21, 2019
b01143a
spec: Update tests
woodruffw Jul 21, 2019
9a1ec5c
spec, src: find -> find_by
woodruffw Jul 25, 2019
bee43b9
user, group: Document properties
woodruffw Jul 26, 2019
77d5a1f
user, group: Avoid interpolation in to_s
woodruffw Jul 26, 2019
dc96d4e
user, group: Toplevel docs, add types to params
woodruffw Jul 26, 2019
c199140
unix/{user,group}: Don't raise on invalid UInt32
woodruffw Jul 26, 2019
aa2cecb
unix/group: Optimize extract_members
woodruffw Jul 26, 2019
2345d55
src, spec: Rename User#directory -> #home_directory
woodruffw Jul 26, 2019
e2d8187
spec/user: Add test for #user, formatting
woodruffw Jul 26, 2019
3f5081a
src, spec: Rename #user_id -> #id
woodruffw Jul 26, 2019
712166b
spec/group: Formatting
woodruffw Jul 26, 2019
4c96a6f
spec/{user,group}: Split describes by parameters
woodruffw Jul 26, 2019
856b657
spec/group: Typo
woodruffw Jul 26, 2019
1f4179d
src, spec: Remove Group#members
woodruffw Jul 27, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions bin/ci
Expand Up @@ -129,6 +129,8 @@ with_build_env() {
--rm -t \
-u $(id -u) \
-v $PWD:/mnt \
-v /etc/passwd:/etc/passwd \
-v /etc/group:/etc/group \
-w /mnt \
-e CRYSTAL_CACHE_DIR="/tmp/crystal" \
"$DOCKER_TEST_IMAGE" \
Expand Down
87 changes: 87 additions & 0 deletions spec/std/system/group_spec.cr
@@ -0,0 +1,87 @@
require "spec"
require "system/group"

GROUP_NAME = {{ `id -gn`.stringify.chomp }}
GROUP_ID = {{ `id -g`.stringify.chomp }}

describe System::Group do
describe ".find_by(*, name)" do
it "returns a group by name" do
group = System::Group.find_by(name: GROUP_NAME)

group.should be_a(System::Group)
group.name.should eq(GROUP_NAME)
group.id.should eq(GROUP_ID)
end

it "raises on nonexistent group" do
expect_raises System::Group::NotFoundError, "No such group" do
System::Group.find_by(name: "this_group_does_not_exist")
end
end
end

describe ".find_by(*, id)" do
it "returns a group by id" do
group = System::Group.find_by(id: GROUP_ID)

group.should be_a(System::Group)
group.id.should eq(GROUP_ID)
group.name.should eq(GROUP_NAME)
end

it "raises on nonexistent group name" do
expect_raises System::Group::NotFoundError, "No such group" do
System::Group.find_by(id: "1234567")
end
end
end

describe ".find_by?(*, name)" do
it "returns a group by name" do
group = System::Group.find_by?(name: GROUP_NAME).not_nil!

group.should be_a(System::Group)
group.name.should eq(GROUP_NAME)
group.id.should eq(GROUP_ID)
end

it "returns nil on nonexistent group" do
group = System::Group.find_by?(name: "this_group_does_not_exist")
group.should eq(nil)
end
end

describe ".find_by?(*, id)" do
it "returns a group by id" do
group = System::Group.find_by?(id: GROUP_ID).not_nil!

group.should be_a(System::Group)
group.id.should eq(GROUP_ID)
group.name.should eq(GROUP_NAME)
end

it "returns nil on nonexistent group id" do
group = System::Group.find_by?(id: "1234567")
group.should eq(nil)
end
end

describe "#name" do
it "is the same as the source name" do
System::Group.find_by(name: GROUP_NAME).name.should eq(GROUP_NAME)
end
end

describe "#id" do
it "is the same as the source ID" do
System::Group.find_by(id: GROUP_ID).id.should eq(GROUP_ID)
end
end

describe "#to_s" do
it "returns a string representation" do
System::Group.find_by(name: GROUP_NAME).to_s.should eq("#{GROUP_NAME} (#{GROUP_ID})")
end
end
end
111 changes: 111 additions & 0 deletions spec/std/system/user_spec.cr
@@ -0,0 +1,111 @@
require "spec"
require "system/user"

USER_NAME = {{ `id -un`.stringify.chomp }}
USER_ID = {{ `id -u`.stringify.chomp }}

describe System::User do
describe ".find_by(*, name)" do
it "returns a user by name" do
user = System::User.find_by(name: USER_NAME)

user.should be_a(System::User)
user.username.should eq(USER_NAME)
user.id.should eq(USER_ID)
end

it "raises on a nonexistent user" do
expect_raises System::User::NotFoundError, "No such user" do
System::User.find_by(name: "this_user_does_not_exist")
end
end
end

describe ".find_by(*, id)" do
it "returns a user by id" do
user = System::User.find_by(id: USER_ID)

user.should be_a(System::User)
user.id.should eq(USER_ID)
user.username.should eq(USER_NAME)
end

it "raises on nonexistent user id" do
expect_raises System::User::NotFoundError, "No such user" do
System::User.find_by(id: "1234567")
end
end
end

describe ".find_by?(*, name)" do
it "returns a user by name" do
user = System::User.find_by?(name: USER_NAME).not_nil!

user.should be_a(System::User)
user.username.should eq(USER_NAME)
user.id.should eq(USER_ID)
end

it "returns nil on nonexistent user" do
user = System::User.find_by?(name: "this_user_does_not_exist")
user.should eq(nil)
end
end

describe ".find_by?(*, id)" do
it "returns a user by id" do
user = System::User.find_by?(id: USER_ID).not_nil!

user.should be_a(System::User)
user.id.should eq(USER_ID)
user.username.should eq(USER_NAME)
end

it "returns nil on nonexistent user id" do
user = System::User.find_by?(id: "1234567")
user.should eq(nil)
end
end

describe "#username" do
it "is the same as the source name" do
System::User.find_by(name: USER_NAME).username.should eq(USER_NAME)
end
end

describe "#id" do
it "is the same as the source ID" do
System::User.find_by(id: USER_ID).id.should eq(USER_ID)
end
end

describe "#group_id" do
it "calls without raising" do
System::User.find_by(name: USER_NAME).group_id
end
end

describe "#name" do
it "calls without raising" do
System::User.find_by(name: USER_NAME).name
end
end

describe "#home_directory" do
it "calls without raising" do
System::User.find_by(name: USER_NAME).home_directory
end
end

describe "#shell" do
it "calls without raising" do
System::User.find_by(name: USER_NAME).shell
end
end

describe "#to_s" do
it "returns a string representation" do
System::User.find_by(name: USER_NAME).to_s.should eq("#{USER_NAME} (#{USER_ID})")
end
end
end
5 changes: 5 additions & 0 deletions src/crystal/system/group.cr
@@ -0,0 +1,5 @@
{% if flag?(:unix) %}
require "./unix/group"
{% else %}
{% raise "No Crystal::System::Group implementation available" %}
{% end %}
48 changes: 48 additions & 0 deletions src/crystal/system/unix/group.cr
@@ -0,0 +1,48 @@
require "c/grp"

module Crystal::System::Group
private GETGR_R_SIZE_MAX = 1024 * 16

private def from_struct(grp)
new(String.new(grp.gr_name), grp.gr_gid.to_s)
end

private def from_name?(groupname : String)
groupname.check_no_null_byte

grp = uninitialized LibC::Group
grp_pointer = pointerof(grp)
initial_buf = uninitialized UInt8[1024]
buf = initial_buf.to_slice

ret = LibC.getgrnam_r(groupname, grp_pointer, buf, buf.size, pointerof(grp_pointer))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can out be used here instead if taking pointer of pointer? Also, in examples I see they use two variables and they don't point to each other, but I'm not familiar with this API.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bcardiff Yes, it's what I suggested. I forgot to come back to this because it's not possible due to a compiler bug. out will fail if there's already a variable declared with that name. That means that you can't use out twice inside a same method. It's something that shouldn't be hard to fix, but right now it prevents us from using out here.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, this is what I ran into.

while ret == LibC::ERANGE && buf.size < GETGR_R_SIZE_MAX
buf = Bytes.new(buf.size * 2)
ret = LibC.getgrnam_r(groupname, grp_pointer, buf, buf.size, pointerof(grp_pointer))
end

raise Errno.new("getgrnam_r") if ret != 0

from_struct(grp) if grp_pointer
end

private def from_id?(groupid : String)
groupid = groupid.to_u32?
return unless groupid

grp = uninitialized LibC::Group
grp_pointer = pointerof(grp)
initial_buf = uninitialized UInt8[1024]
buf = initial_buf.to_slice

ret = LibC.getgrgid_r(groupid, grp_pointer, buf, buf.size, pointerof(grp_pointer))
while ret == LibC::ERANGE && buf.size < GETGR_R_SIZE_MAX
buf = Bytes.new(buf.size * 2)
ret = LibC.getgrgid_r(groupid, grp_pointer, buf, buf.size, pointerof(grp_pointer))
end

raise Errno.new("getgrgid_r") if ret != 0

from_struct(grp) if grp_pointer
end
end
49 changes: 49 additions & 0 deletions src/crystal/system/unix/user.cr
@@ -0,0 +1,49 @@
require "c/pwd"

module Crystal::System::User
private GETPW_R_SIZE_MAX = 1024 * 16

private def from_struct(pwd)
user = String.new(pwd.pw_gecos).split(",").first
new(String.new(pwd.pw_name), pwd.pw_uid.to_s, pwd.pw_gid.to_s, user, String.new(pwd.pw_dir), String.new(pwd.pw_shell))
end

private def from_username?(username : String)
username.check_no_null_byte

pwd = uninitialized LibC::Passwd
pwd_pointer = pointerof(pwd)
initial_buf = uninitialized UInt8[1024]
buf = initial_buf.to_slice

ret = LibC.getpwnam_r(username, pwd_pointer, buf, buf.size, pointerof(pwd_pointer))
while ret == LibC::ERANGE && buf.size < GETPW_R_SIZE_MAX
buf = Bytes.new(buf.size * 2)
ret = LibC.getpwnam_r(username, pwd_pointer, buf, buf.size, pointerof(pwd_pointer))
end

raise Errno.new("getpwnam_r") if ret != 0

from_struct(pwd) if pwd_pointer
end

private def from_id?(id : String)
id = id.to_u32?
return unless id

pwd = uninitialized LibC::Passwd
pwd_pointer = pointerof(pwd)
initial_buf = uninitialized UInt8[1024]
buf = initial_buf.to_slice

ret = LibC.getpwuid_r(id, pwd_pointer, buf, buf.size, pointerof(pwd_pointer))
while ret == LibC::ERANGE && buf.size < GETPW_R_SIZE_MAX
buf = Bytes.new(buf.size * 2)
ret = LibC.getpwuid_r(id, pwd_pointer, buf, buf.size, pointerof(pwd_pointer))
end

raise Errno.new("getpwuid_r") if ret != 0

from_struct(pwd) if pwd_pointer
end
end
5 changes: 5 additions & 0 deletions src/crystal/system/user.cr
@@ -0,0 +1,5 @@
{% if flag?(:unix) %}
require "./unix/user"
{% else %}
{% raise "No Crystal::System::User implementation available" %}
{% end %}
1 change: 1 addition & 0 deletions src/docs_main.cr
Expand Up @@ -62,3 +62,4 @@ require "./uuid"
require "./uuid/json"
require "./zip"
require "./zlib"
require "./system/*"
11 changes: 11 additions & 0 deletions src/lib_c/aarch64-linux-gnu/c/grp.cr
@@ -0,0 +1,11 @@
lib LibC
struct Group
gr_name : Char*
gr_passwd : Char*
gr_gid : GidT
gr_mem : Char**
end

fun getgrnam_r(name : Char*, grp : Group*, buf : Char*, bufsize : SizeT, result : Group**) : Int
fun getgrgid_r(gid : GidT, grp : Group*, buf : Char*, bufsize : SizeT, result : Group**) : Int
end
14 changes: 14 additions & 0 deletions src/lib_c/aarch64-linux-gnu/c/pwd.cr
@@ -0,0 +1,14 @@
lib LibC
struct Passwd
pw_name : Char*
pw_passwd : Char*
pw_uid : UidT
pw_gid : GidT
pw_gecos : Char*
pw_dir : Char*
pw_shell : Char*
end

fun getpwnam_r(login : Char*, pwstore : Passwd*, buf : Char*, bufsize : SizeT, result : Passwd**) : Int
fun getpwuid_r(uid : UidT, pwstore : Passwd*, buf : Char*, bufsize : SizeT, result : Passwd**) : Int
end
11 changes: 11 additions & 0 deletions src/lib_c/aarch64-linux-musl/c/grp.cr
@@ -0,0 +1,11 @@
lib LibC
struct Group
gr_name : Char*
gr_passwd : Char*
gr_gid : GidT
gr_mem : Char**
end

fun getgrnam_r(name : Char*, grp : Group*, buf : Char*, bufsize : SizeT, result : Group**) : Int
fun getgrgid_r(gid : GidT, grp : Group*, buf : Char*, bufsize : SizeT, result : Group**) : Int
end
14 changes: 14 additions & 0 deletions src/lib_c/aarch64-linux-musl/c/pwd.cr
@@ -0,0 +1,14 @@
lib LibC
struct Passwd
pw_name : Char*
pw_passwd : Char*
pw_uid : UidT
pw_gid : GidT
pw_gecos : Char*
pw_dir : Char*
pw_shell : Char*
end

fun getpwnam_r(login : Char*, pwstore : Passwd*, buf : Char*, bufsize : SizeT, result : Passwd**) : Int
fun getpwuid_r(uid : UidT, pwstore : Passwd*, buf : Char*, bufsize : SizeT, result : Passwd**) : Int
end
11 changes: 11 additions & 0 deletions src/lib_c/arm-linux-gnueabihf/c/grp.cr
@@ -0,0 +1,11 @@
lib LibC
struct Group
gr_name : Char*
gr_passwd : Char*
gr_gid : GidT
gr_mem : Char**
end

fun getgrnam_r(name : Char*, grp : Group*, buf : Char*, bufsize : SizeT, result : Group**) : Int
fun getgrgid_r(gid : GidT, grp : Group*, buf : Char*, bufsize : SizeT, result : Group**) : Int
end